From c83aefabfa626fc486e98c2b6c667d51a809011e Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Mon, 9 Mar 2026 19:11:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E5=8F=91=E7=8E=B0=E7=9A=84=E6=89=80=E6=9C=89?= =?UTF-8?q?=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical 级别: - zhipu.ts: 添加 API 响应边界检查 - DictionaryClient.tsx: 添加 entries 数组边界检查 - subtitleParser.ts: 修复 getNearestIndex 逻辑错误 High 级别: - text-speaker/page.tsx: 修复非空断言和 ref 检查 - folder-repository.ts: 添加 user 关系 null 检查 Medium 级别: - InFolder.tsx: 修复 throw result.message 为 throw new Error() - localStorageOperators.ts: 返回类型改为 T | null,添加 schema 验证 - SaveList.tsx: 处理 data 可能为 null 的情况 --- .../dictionary/DictionaryClient.tsx | 10 ++--- .../srt-player/utils/subtitleParser.ts | 9 ++-- src/app/(features)/text-speaker/SaveList.tsx | 41 ++++++++----------- src/app/(features)/text-speaker/page.tsx | 8 ++-- src/app/folders/[folder_id]/InFolder.tsx | 22 +++++++--- src/lib/bigmodel/zhipu.ts | 7 +++- src/lib/browser/localStorageOperators.ts | 25 +++++++---- src/modules/folder/folder-repository.ts | 12 +++--- 8 files changed, 75 insertions(+), 59 deletions(-) diff --git a/src/app/(features)/dictionary/DictionaryClient.tsx b/src/app/(features)/dictionary/DictionaryClient.tsx index 910a9f9..b213d59 100644 --- a/src/app/(features)/dictionary/DictionaryClient.tsx +++ b/src/app/(features)/dictionary/DictionaryClient.tsx @@ -90,11 +90,11 @@ export function DictionaryClient({ initialFolders }: DictionaryClientProps) { const folderSelect = document.getElementById("folder-select") as HTMLSelectElement; const folderId = folderSelect?.value ? Number(folderSelect.value) : folders[0]?.id; - if (!searchResult) return; + if (!searchResult?.entries?.length) return; - const definition = searchResult.entries.reduce((p, e) => { - return { ...p, definition: p.definition + ' | ' + e.definition }; - }).definition; + const definition = searchResult.entries + .map((e) => e.definition) + .join(" | "); try { await actionCreatePair({ @@ -102,7 +102,7 @@ export function DictionaryClient({ initialFolders }: DictionaryClientProps) { text2: definition, language1: queryLang, language2: definitionLang, - ipa1: searchResult.entries[0].ipa, + ipa1: searchResult.entries[0]?.ipa, folderId: folderId, }); diff --git a/src/app/(features)/srt-player/utils/subtitleParser.ts b/src/app/(features)/srt-player/utils/subtitleParser.ts index b49eba6..17246df 100644 --- a/src/app/(features)/srt-player/utils/subtitleParser.ts +++ b/src/app/(features)/srt-player/utils/subtitleParser.ts @@ -62,13 +62,12 @@ export function getNearestIndex( ): number | null { for (let i = 0; i < subtitles.length; i++) { const subtitle = subtitles[i]; - const isBefore = currentTime - subtitle.start >= 0; - const isAfter = currentTime - subtitle.end >= 0; + const isWithin = currentTime >= subtitle.start && currentTime <= subtitle.end; - if (!isBefore || !isAfter) return i - 1; - if (isBefore && !isAfter) return i; + if (isWithin) return i; + if (currentTime < subtitle.start) return i > 0 ? i - 1 : null; } - return null; + return subtitles.length > 0 ? subtitles.length - 1 : null; } export function getCurrentSubtitle( diff --git a/src/app/(features)/text-speaker/SaveList.tsx b/src/app/(features)/text-speaker/SaveList.tsx index 98674f5..a031bb5 100644 --- a/src/app/(features)/text-speaker/SaveList.tsx +++ b/src/app/(features)/text-speaker/SaveList.tsx @@ -60,11 +60,12 @@ export function SaveList({ show = false, handleUse }: SaveListProps) { const [data, setData] = useState(getFromLocalStorage()); const handleDel = (item: z.infer) => { const current_data = getFromLocalStorage(); + if (!current_data) return; - current_data.splice( - current_data.findIndex((v) => v.text === item.text), - 1, - ); + const index = current_data.findIndex((v) => v.text === item.text); + if (index === -1) return; + + current_data.splice(index, 1); setIntoLocalStorage(current_data); refresh(); }; @@ -78,33 +79,25 @@ export function SaveList({ show = false, handleUse }: SaveListProps) { refresh(); } }; - if (show) + if (show && data) return (
-
- - +

{t("saved")}

+
-
    - {data.map((v) => ( +
      + {data.map((item, i) => ( diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index d683e47..9bfdc89 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -48,8 +48,8 @@ export default function TextSpeakerPage() { const handleEnded = () => { if (autopause) { setPause(true); - } else { - load(objurlRef.current!); + } else if (objurlRef.current) { + load(objurlRef.current); play(); } }; @@ -187,7 +187,7 @@ export default function TextSpeakerPage() { theIPA = tmp_ipa; } - const save = getFromLocalStorage(); + const save = getFromLocalStorage() ?? []; const oldIndex = save.findIndex((v) => v.text === textRef.current); if (oldIndex !== -1) { const oldItem = save[oldIndex]; @@ -293,7 +293,7 @@ export default function TextSpeakerPage() { size="lg" onClick={() => { setAutopause(!autopause); - if (objurlRef) { + if (objurlRef.current) { stop(); } setPause(true); diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index f13a3c2..d757653 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -26,10 +26,14 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl setLoading(true); await actionGetPairsByFolderId(folderId) .then(result => { - if (!result.success || !result.data) throw result.message; + if (!result.success || !result.data) { + throw new Error(result.message || "Failed to load text pairs"); + } return result.data; }).then(setTextPairs) - .catch(toast.error) + .catch((error) => { + toast.error(error instanceof Error ? error.message : "Unknown error"); + }) .finally(() => { setLoading(false); }); @@ -40,10 +44,14 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl const refreshTextPairs = async () => { await actionGetPairsByFolderId(folderId) .then(result => { - if (!result.success || !result.data) throw result.message; + if (!result.success || !result.data) { + throw new Error(result.message || "Failed to refresh text pairs"); + } return result.data; }).then(setTextPairs) - .catch(toast.error); + .catch((error) => { + toast.error(error instanceof Error ? error.message : "Unknown error"); + }); }; return ( @@ -119,9 +127,11 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl onDel={() => { actionDeletePairById(textPair.id) .then(result => { - if (!result.success) throw result.message; + if (!result.success) throw new Error(result.message || "Delete failed"); }).then(refreshTextPairs) - .catch(toast.error); + .catch((error) => { + toast.error(error instanceof Error ? error.message : "Unknown error"); + }); }} refreshTextPairs={refreshTextPairs} /> diff --git a/src/lib/bigmodel/zhipu.ts b/src/lib/bigmodel/zhipu.ts index e617750..0892459 100644 --- a/src/lib/bigmodel/zhipu.ts +++ b/src/lib/bigmodel/zhipu.ts @@ -47,7 +47,12 @@ async function getAnswer(prompt: string | Messages): Promise { : prompt; const response = await callZhipuAPI(messages); - return response.choices[0].message.content.trim() as string; + + if (!response.choices?.[0]?.message?.content) { + throw new Error("AI API 返回空响应"); + } + + return response.choices[0].message.content.trim(); } export { getAnswer }; diff --git a/src/lib/browser/localStorageOperators.ts b/src/lib/browser/localStorageOperators.ts index 6139cd5..240ee1b 100644 --- a/src/lib/browser/localStorageOperators.ts +++ b/src/lib/browser/localStorageOperators.ts @@ -1,7 +1,9 @@ +"use client"; + import { z } from "zod"; interface LocalStorageOperator { - get: () => T; + get: () => T | null; set: (value: T) => void; } @@ -9,22 +11,29 @@ export function getLocalStorageOperator( key: string, schema: T ): LocalStorageOperator> { - const get = (): z.infer => { + const get = (): z.infer | null => { if (typeof window === "undefined") { - return [] as unknown as z.infer; + return null; } try { const item = localStorage.getItem(key); if (item === null) { - return [] as unknown as z.infer; + return null; } const parsed = JSON.parse(item); - return schema.parse(parsed); + const result = schema.safeParse(parsed); + + if (!result.success) { + console.warn(`[localStorage] Schema validation failed for key "${key}":`, result.error.message); + return null; + } + + return result.data; } catch (error) { - console.error(`Error reading from localStorage key "${key}":`, error); - return [] as unknown as z.infer; + console.error(`[localStorage] Error reading key "${key}":`, error instanceof Error ? error.message : String(error)); + return null; } }; @@ -36,7 +45,7 @@ export function getLocalStorageOperator( try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { - console.error(`Error writing to localStorage key "${key}":`, error); + console.error(`[localStorage] Error writing key "${key}":`, error instanceof Error ? error.message : String(error)); } }; diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts index df3cb27..f046a8f 100644 --- a/src/modules/folder/folder-repository.ts +++ b/src/modules/folder/folder-repository.ts @@ -192,8 +192,8 @@ export async function repoGetPublicFolders( visibility: folder.visibility, createdAt: folder.createdAt, userId: folder.userId, - userName: folder.user.name, - userUsername: folder.user.username, + userName: folder.user?.name ?? "Unknown", + userUsername: folder.user?.username ?? "unknown", totalPairs: folder._count.pairs, favoriteCount: folder._count.favorites, })); @@ -221,8 +221,8 @@ export async function repoSearchPublicFolders( visibility: folder.visibility, createdAt: folder.createdAt, userId: folder.userId, - userName: folder.user.name, - userUsername: folder.user.username, + userName: folder.user?.name ?? "Unknown", + userUsername: folder.user?.username ?? "unknown", totalPairs: folder._count.pairs, favoriteCount: folder._count.favorites, })); @@ -300,8 +300,8 @@ export async function repoGetUserFavorites(input: RepoInputGetUserFavorites) { folderCreatedAt: fav.folder.createdAt, folderTotalPairs: fav.folder._count.pairs, folderOwnerId: fav.folder.userId, - folderOwnerName: fav.folder.user.name, - folderOwnerUsername: fav.folder.user.username, + folderOwnerName: fav.folder.user?.name ?? "Unknown", + folderOwnerUsername: fav.folder.user?.username ?? "unknown", favoritedAt: fav.createdAt, })); }