...
This commit is contained in:
@@ -24,11 +24,7 @@
|
||||
"noFoldersYet": "No folders yet",
|
||||
"folderInfo": "{id}. {name} ({totalPairs})",
|
||||
"enterFolderName": "Enter folder name:",
|
||||
"confirmDelete": "Type \"{name}\" to delete:",
|
||||
"createFolderSuccess": "Folder created successfully",
|
||||
"deleteFolderSuccess": "Folder deleted successfully",
|
||||
"createFolderError": "Failed to create folder",
|
||||
"deleteFolderError": "Failed to delete folder"
|
||||
"confirmDelete": "Type \"{name}\" to delete:"
|
||||
},
|
||||
"folder_id": {
|
||||
"unauthorized": "You are not the owner of this folder",
|
||||
@@ -82,10 +78,6 @@
|
||||
"description": "Under development, stay tuned"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"loading": "Loading...",
|
||||
"githubLogin": "GitHub Login"
|
||||
},
|
||||
"auth": {
|
||||
"title": "Authentication",
|
||||
"signIn": "Sign In",
|
||||
@@ -93,18 +85,11 @@
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"name": "Name",
|
||||
"signInButton": "Sign In",
|
||||
"signUpButton": "Sign Up",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"signInWithGitHub": "Sign In with GitHub",
|
||||
"signUpWithGitHub": "Sign Up with GitHub",
|
||||
"invalidEmail": "Please enter a valid email address",
|
||||
"passwordTooShort": "Password must be at least 8 characters",
|
||||
"passwordsNotMatch": "Passwords do not match",
|
||||
"signInFailed": "Sign in failed, please check your email and password",
|
||||
"signUpFailed": "Sign up failed, please try again later",
|
||||
"nameRequired": "Please enter your name",
|
||||
"emailRequired": "Please enter your email",
|
||||
"passwordRequired": "Please enter your password",
|
||||
@@ -151,42 +136,10 @@
|
||||
"next": "Next",
|
||||
"restart": "Restart",
|
||||
"autoPause": "Auto Pause ({enabled})",
|
||||
"playbackSpeed": "Playback Speed",
|
||||
"subtitleSettings": "Subtitle Settings",
|
||||
"fontSize": "Font Size",
|
||||
"backgroundColor": "Background Color",
|
||||
"textColor": "Text Color",
|
||||
"fontFamily": "Font Family",
|
||||
"opacity": "Opacity",
|
||||
"position": "Position",
|
||||
"top": "Top",
|
||||
"center": "Center",
|
||||
"bottom": "Bottom",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"uploadVideoAndSubtitle": "Please upload video and subtitle files",
|
||||
"uploadVideoFile": "Please upload video file",
|
||||
"uploadSubtitleFile": "Please upload subtitle file",
|
||||
"processingSubtitle": "Processing subtitle file...",
|
||||
"needBothFiles": "Both video and subtitle files are required to start learning",
|
||||
"videoFile": "Video File",
|
||||
"subtitleFile": "Subtitle File",
|
||||
"uploaded": "Uploaded",
|
||||
"notUploaded": "Not Uploaded",
|
||||
"upload": "Upload",
|
||||
"autoPauseStatus": "Auto Pause: {enabled}",
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"videoUploadFailed": "Video upload failed",
|
||||
"subtitleUploadFailed": "Subtitle upload failed",
|
||||
"subtitleLoadSuccess": "Subtitle file loaded successfully",
|
||||
"subtitleLoadFailed": "Subtitle file loading failed",
|
||||
"shortcuts": {
|
||||
"playPause": "Play/Pause",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"restart": "Restart",
|
||||
"autoPause": "Toggle Auto Pause"
|
||||
}
|
||||
"subtitleUploadFailed": "Subtitle upload failed"
|
||||
},
|
||||
"text_speaker": {
|
||||
"generateIPA": "Generate IPA",
|
||||
|
||||
@@ -24,11 +24,7 @@
|
||||
"noFoldersYet": "还没有文件夹",
|
||||
"folderInfo": "{id}. {name} ({totalPairs})",
|
||||
"enterFolderName": "输入文件夹名称:",
|
||||
"confirmDelete": "输入 \"{name}\" 以删除:",
|
||||
"createFolderSuccess": "文件夹创建成功",
|
||||
"deleteFolderSuccess": "文件夹删除成功",
|
||||
"createFolderError": "创建文件夹失败",
|
||||
"deleteFolderError": "删除文件夹失败"
|
||||
"confirmDelete": "输入 \"{name}\" 以删除:"
|
||||
},
|
||||
"folder_id": {
|
||||
"unauthorized": "您不是此文件夹的所有者",
|
||||
@@ -82,10 +78,6 @@
|
||||
"description": "开发中,敬请期待"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"loading": "加载中...",
|
||||
"githubLogin": "GitHub登录"
|
||||
},
|
||||
"auth": {
|
||||
"title": "登录",
|
||||
"signIn": "登录",
|
||||
@@ -93,28 +85,18 @@
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"name": "用户名",
|
||||
"signInButton": "登录",
|
||||
"signUpButton": "注册",
|
||||
"noAccount": "还没有账户?",
|
||||
"hasAccount": "已有账户?",
|
||||
"signInWithGitHub": "使用GitHub登录",
|
||||
"signUpWithGitHub": "使用GitHub注册",
|
||||
"invalidEmail": "请输入有效的邮箱地址",
|
||||
"passwordTooShort": "密码至少需要8个字符",
|
||||
"passwordsNotMatch": "两次输入的密码不匹配",
|
||||
"signInFailed": "登录失败,请检查您的邮箱和密码",
|
||||
"signUpFailed": "注册失败,请稍后再试",
|
||||
"nameRequired": "请输入用户名",
|
||||
"emailRequired": "请输入邮箱",
|
||||
"passwordRequired": "请输入密码",
|
||||
"confirmPasswordRequired": "请确认密码"
|
||||
"confirmPasswordRequired": "请确认密码",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"memorize": {
|
||||
"choose": {
|
||||
"back": "返回",
|
||||
"choose": "选择"
|
||||
},
|
||||
"folder_selector": {
|
||||
"selectFolder": "选择文件夹",
|
||||
"noFolders": "未找到文件夹",
|
||||
@@ -155,41 +137,10 @@
|
||||
"next": "下句",
|
||||
"restart": "句首",
|
||||
"autoPause": "自动暂停({enabled})",
|
||||
"playbackSpeed": "播放速度",
|
||||
"subtitleSettings": "字幕设置",
|
||||
"fontSize": "字体大小",
|
||||
"backgroundColor": "背景颜色",
|
||||
"textColor": "文字颜色",
|
||||
"fontFamily": "字体",
|
||||
"opacity": "透明度",
|
||||
"position": "位置",
|
||||
"top": "顶部",
|
||||
"center": "居中",
|
||||
"bottom": "底部",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"uploadVideoAndSubtitle": "请上传视频和字幕文件",
|
||||
"uploadVideoFile": "请上传视频文件",
|
||||
"uploadSubtitleFile": "请上传字幕文件",
|
||||
"processingSubtitle": "字幕文件正在处理中...",
|
||||
"needBothFiles": "需要同时上传视频和字幕文件才能开始学习",
|
||||
"videoFile": "视频文件",
|
||||
"subtitleFile": "字幕文件",
|
||||
"uploaded": "已上传",
|
||||
"notUploaded": "未上传",
|
||||
"autoPauseStatus": "自动暂停: {enabled}",
|
||||
"on": "开",
|
||||
"off": "关",
|
||||
"videoUploadFailed": "视频上传失败",
|
||||
"subtitleUploadFailed": "字幕上传失败",
|
||||
"subtitleLoadSuccess": "字幕文件加载成功",
|
||||
"subtitleLoadFailed": "字幕文件加载失败",
|
||||
"shortcuts": {
|
||||
"playPause": "播放/暂停",
|
||||
"next": "下一句",
|
||||
"previous": "上一句",
|
||||
"restart": "句首",
|
||||
"autoPause": "切换自动暂停"
|
||||
}
|
||||
"subtitleUploadFailed": "字幕上传失败"
|
||||
},
|
||||
"text_speaker": {
|
||||
"generateIPA": "生成IPA",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SubtitleEntry } from "../types/subtitle";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
export function parseSrt(data: string): SubtitleEntry[] {
|
||||
const lines = data.split(/\r?\n/);
|
||||
@@ -93,7 +94,7 @@ export async function loadSubtitle(url: string): Promise<SubtitleEntry[]> {
|
||||
const data = await response.text();
|
||||
return parseSrt(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load subtitle:', error);
|
||||
logger.error('加载字幕失败', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { useTranslations } from "next-intl";
|
||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||
import { genIPA, genLocale } from "@/lib/server/translatorActions";
|
||||
import { logger } from "@/lib/logger";
|
||||
import PageLayout from "@/components/ui/PageLayout";
|
||||
|
||||
export default function TextSpeakerPage() {
|
||||
@@ -75,7 +76,7 @@ export default function TextSpeakerPage() {
|
||||
setIPA(data.ipa);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
logger.error("生成 IPA 失败", e);
|
||||
setIPA("");
|
||||
});
|
||||
}
|
||||
@@ -96,7 +97,6 @@ export default function TextSpeakerPage() {
|
||||
try {
|
||||
let theLocale = locale;
|
||||
if (!theLocale) {
|
||||
console.log("downloading text info");
|
||||
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
||||
setLocale(tmp_locale);
|
||||
theLocale = tmp_locale;
|
||||
@@ -123,8 +123,7 @@ export default function TextSpeakerPage() {
|
||||
load(objurlRef.current);
|
||||
play();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
logger.error("播放音频失败", e);
|
||||
setPause(true);
|
||||
setLocale(null);
|
||||
|
||||
@@ -181,7 +180,6 @@ export default function TextSpeakerPage() {
|
||||
try {
|
||||
let theLocale = locale;
|
||||
if (!theLocale) {
|
||||
console.log("downloading text info");
|
||||
const tmp_locale = await genLocale(textRef.current.slice(0, 30));
|
||||
setLocale(tmp_locale);
|
||||
theLocale = tmp_locale;
|
||||
@@ -218,7 +216,7 @@ export default function TextSpeakerPage() {
|
||||
}
|
||||
setIntoLocalStorage(save);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error("保存到本地存储失败", e);
|
||||
setLocale(null);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { TranslationHistorySchema } from "@/lib/interfaces";
|
||||
import { tlsoPush, tlso } from "@/lib/browser/localStorageOperators";
|
||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { Plus, Trash } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -67,7 +68,7 @@ export default function TranslatorPage() {
|
||||
lastTTS.current.url = url;
|
||||
} catch (error) {
|
||||
toast.error("Failed to generate audio");
|
||||
console.error(error);
|
||||
logger.error("生成音频失败", error);
|
||||
}
|
||||
}
|
||||
await play();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Folder } from "../../../generated/prisma/browser";
|
||||
import {
|
||||
@@ -101,7 +102,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
logger.error("加载文件夹失败", error);
|
||||
toast.error("加载出错,请重试。");
|
||||
});
|
||||
}, [userId]);
|
||||
@@ -111,7 +112,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
||||
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
|
||||
setFolders(updatedFolders);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error("更新文件夹失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import Input from "@/components/ui/Input";
|
||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||
import { X } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LOCALES } from "@/config/locales";
|
||||
|
||||
interface AddTextPairModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -16,53 +16,6 @@ interface AddTextPairModalProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
const COMMON_LOCALES = [
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
{ label: "英文", value: "en-US" },
|
||||
{ label: "意大利语", value: "it-IT" },
|
||||
{ label: "日语", value: "ja-JP" },
|
||||
{ label: "其他", value: "other" },
|
||||
];
|
||||
|
||||
interface LocaleSelectorProps {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
}
|
||||
|
||||
function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other");
|
||||
const showFullList = value === "other" || !isCommonLocale;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
value={isCommonLocale ? value : "other"}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||
>
|
||||
{COMMON_LOCALES.map((locale) => (
|
||||
<option key={locale.value} value={locale.value}>
|
||||
{locale.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{showFullList && (
|
||||
<select
|
||||
value={value === "other" ? LOCALES[0] : value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
|
||||
>
|
||||
{LOCALES.map((locale) => (
|
||||
<option key={locale} value={locale}>
|
||||
{locale}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AddTextPairModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
||||
@@ -13,6 +13,7 @@ import TextPairCard from "./TextPairCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
import PageLayout from "@/components/ui/PageLayout";
|
||||
import { GreenButton } from "@/components/ui/buttons";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { IconButton } from "@/components/ui/buttons";
|
||||
import CardList from "@/components/ui/CardList";
|
||||
|
||||
@@ -38,7 +39,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
const data = await getPairsByFolderId(folderId);
|
||||
setTextPairs(data as TextPair[]);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch text pairs:", error);
|
||||
logger.error("获取文本对失败", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
const data = await getPairsByFolderId(folderId);
|
||||
setTextPairs(data as TextPair[]);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch text pairs:", error);
|
||||
logger.error("获取文本对失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import Input from "@/components/ui/Input";
|
||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||
import { X } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { PairUpdateInput } from "../../../../generated/prisma/models";
|
||||
import { TextPair } from "./InFolder";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -22,23 +23,23 @@ export default function UpdateTextPairModal({
|
||||
const t = useTranslations("folder_id");
|
||||
const input1Ref = useRef<HTMLInputElement>(null);
|
||||
const input2Ref = useRef<HTMLInputElement>(null);
|
||||
const input3Ref = useRef<HTMLInputElement>(null);
|
||||
const input4Ref = useRef<HTMLInputElement>(null);
|
||||
const [locale1, setLocale1] = useState(textPair.locale1);
|
||||
const [locale2, setLocale2] = useState(textPair.locale2);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (
|
||||
!input1Ref.current?.value ||
|
||||
!input2Ref.current?.value ||
|
||||
!input3Ref.current?.value ||
|
||||
!input4Ref.current?.value
|
||||
!locale1 ||
|
||||
!locale2
|
||||
)
|
||||
return;
|
||||
|
||||
const text1 = input1Ref.current.value;
|
||||
const text2 = input2Ref.current.value;
|
||||
const locale1 = input3Ref.current.value;
|
||||
const locale2 = input4Ref.current.value;
|
||||
|
||||
if (
|
||||
typeof text1 === "string" &&
|
||||
typeof text2 === "string" &&
|
||||
@@ -50,8 +51,6 @@ export default function UpdateTextPairModal({
|
||||
locale2.trim() !== ""
|
||||
) {
|
||||
onUpdate(textPair.id, { text1, text2, locale1, locale2 });
|
||||
input1Ref.current.value = "";
|
||||
input2Ref.current.value = "";
|
||||
}
|
||||
};
|
||||
return (
|
||||
@@ -90,19 +89,11 @@ export default function UpdateTextPairModal({
|
||||
</div>
|
||||
<div>
|
||||
{t("locale1")}
|
||||
<Input
|
||||
defaultValue={textPair.locale1}
|
||||
ref={input3Ref}
|
||||
className="w-full"
|
||||
></Input>
|
||||
<LocaleSelector value={locale1} onChange={setLocale1} />
|
||||
</div>
|
||||
<div>
|
||||
{t("locale2")}
|
||||
<Input
|
||||
defaultValue={textPair.locale2}
|
||||
ref={input4Ref}
|
||||
className="w-full"
|
||||
></Input>
|
||||
<LocaleSelector value={locale2} onChange={setLocale2} />
|
||||
</div>
|
||||
</div>
|
||||
<LightButton onClick={handleUpdate}>{t("update")}</LightButton>
|
||||
|
||||
48
src/components/ui/LocaleSelector.tsx
Normal file
48
src/components/ui/LocaleSelector.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { LOCALES } from "@/config/locales";
|
||||
|
||||
const COMMON_LOCALES = [
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
{ label: "英文", value: "en-US" },
|
||||
{ label: "意大利语", value: "it-IT" },
|
||||
{ label: "日语", value: "ja-JP" },
|
||||
{ label: "其他", value: "other" },
|
||||
];
|
||||
|
||||
interface LocaleSelectorProps {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
}
|
||||
|
||||
export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other");
|
||||
const showFullList = value === "other" || !isCommonLocale;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
value={isCommonLocale ? value : "other"}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||
>
|
||||
{COMMON_LOCALES.map((locale) => (
|
||||
<option key={locale.value} value={locale.value}>
|
||||
{locale.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{showFullList && (
|
||||
<select
|
||||
value={value === "other" ? LOCALES[0] : value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
|
||||
>
|
||||
{LOCALES.map((locale) => (
|
||||
<option key={locale} value={locale}>
|
||||
{locale}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/lib/interfaces";
|
||||
import z from "zod";
|
||||
import { shallowEqual } from "../utils";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||
key: string,
|
||||
@@ -24,14 +25,14 @@ export const getLocalStorageOperator = <T extends z.ZodTypeAny>(
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Invalid data structure in localStorage:",
|
||||
result.error,
|
||||
);
|
||||
return [] as z.infer<T>;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse ${key} data:`, e);
|
||||
logger.error(`Failed to parse ${key} data:`, e);
|
||||
return [] as z.infer<T>;
|
||||
}
|
||||
},
|
||||
|
||||
29
src/lib/logger.ts
Normal file
29
src/lib/logger.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 统一的日志工具
|
||||
* 在生产环境中可以通过环境变量控制日志级别
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user