fix: 修复代码审查发现的所有 bug
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 的情况
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -60,11 +60,12 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
||||
const [data, setData] = useState(getFromLocalStorage());
|
||||
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
||||
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 (
|
||||
<div
|
||||
className="my-4 p-2 mx-4 md:mx-32 border border-gray-200 rounded-lg"
|
||||
style={{ fontFamily: "Times New Roman, serif" }}
|
||||
>
|
||||
<div className="flex flex-row justify-center gap-8 items-center">
|
||||
<IconClick
|
||||
src={IMAGES.refresh}
|
||||
alt="refresh"
|
||||
onClick={refresh}
|
||||
size="lg"
|
||||
className=""
|
||||
></IconClick>
|
||||
<IconClick
|
||||
src={IMAGES.delete}
|
||||
alt="delete"
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<p className="text-sm text-gray-600">{t("saved")}</p>
|
||||
<button
|
||||
onClick={handleDeleteAll}
|
||||
size="lg"
|
||||
className=""
|
||||
></IconClick>
|
||||
className="text-xs text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
{t("clearAll")}
|
||||
</button>
|
||||
</div>
|
||||
<ul>
|
||||
{data.map((v) => (
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{data.map((item, i) => (
|
||||
<TextCard
|
||||
item={v}
|
||||
key={crypto.randomUUID()}
|
||||
key={i}
|
||||
item={item}
|
||||
handleUse={handleUse}
|
||||
handleDel={handleDel}
|
||||
></TextCard>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -47,7 +47,12 @@ async function getAnswer(prompt: string | Messages): Promise<string> {
|
||||
: 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 };
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
interface LocalStorageOperator<T> {
|
||||
get: () => T;
|
||||
get: () => T | null;
|
||||
set: (value: T) => void;
|
||||
}
|
||||
|
||||
@@ -9,22 +11,29 @@ export function getLocalStorageOperator<T extends z.ZodType>(
|
||||
key: string,
|
||||
schema: T
|
||||
): LocalStorageOperator<z.infer<T>> {
|
||||
const get = (): z.infer<T> => {
|
||||
const get = (): z.infer<T> | null => {
|
||||
if (typeof window === "undefined") {
|
||||
return [] as unknown as z.infer<T>;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null) {
|
||||
return [] as unknown as z.infer<T>;
|
||||
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<T>;
|
||||
console.error(`[localStorage] Error reading key "${key}":`, error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +45,7 @@ export function getLocalStorageOperator<T extends z.ZodType>(
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user