diff --git a/.env.example b/.env.example
index 7e831e4..0c535b9 100644
--- a/.env.example
+++ b/.env.example
@@ -11,4 +11,5 @@ GITEA_CLIENT_ID=your_gitea_client_id
GITEA_CLIENT_SECRET=your_gitea_client_secret
GITEA_URL=https://gitea.com
NITRO_PORT=3399
-APP_URL=http://localhost:3399
\ No newline at end of file
+APP_URL=http://localhost:3399
+LOG_LEVEL=info
\ No newline at end of file
diff --git a/app/components/index/CardFormModal.vue b/app/components/index/CardFormModal.vue
index 158d5d5..b1d9744 100644
--- a/app/components/index/CardFormModal.vue
+++ b/app/components/index/CardFormModal.vue
@@ -181,7 +181,7 @@ async function onFileUpload(e: Event) {
})
imageUrl.value = raw.data[0].url
} catch (e) {
- console.log(e)
+ console.error('[CardFormModal] 图片上传失败:', e)
error.value = '图片上传失败'
}
}
diff --git a/app/error.vue b/app/error.vue
new file mode 100644
index 0000000..c2ee6d4
--- /dev/null
+++ b/app/error.vue
@@ -0,0 +1,60 @@
+
+
diff --git a/app/plugins/error-reporter.client.ts b/app/plugins/error-reporter.client.ts
new file mode 100644
index 0000000..ea9e679
--- /dev/null
+++ b/app/plugins/error-reporter.client.ts
@@ -0,0 +1,62 @@
+interface ClientErrorPayload {
+ message: string;
+ stack?: string;
+ url?: string;
+ line?: number;
+ column?: number;
+ userAgent?: string;
+ tags?: string[];
+}
+
+let reportEndpoint = "/api/_error-report";
+
+function reportError(payload: ClientErrorPayload) {
+ try {
+ $fetch(reportEndpoint, {
+ method: "POST",
+ body: payload,
+ }).catch(() => {});
+ } catch {
+ // 静默失败,避免错误上报本身导致循环
+ }
+}
+
+export default defineNuxtPlugin((nuxtApp) => {
+ // Vue 运行时错误
+ nuxtApp.vueApp.config.errorHandler = (err, _instance, info) => {
+ const error = err instanceof Error ? err : new Error(String(err));
+ reportError({
+ message: error.message,
+ stack: error.stack,
+ url: window.location.href,
+ userAgent: navigator.userAgent,
+ tags: ["vue", info],
+ });
+ };
+
+ // 未捕获的 Promise rejection
+ window.addEventListener("unhandledrejection", (event) => {
+ const reason = event.reason;
+ const error = reason instanceof Error ? reason : new Error(String(reason));
+ reportError({
+ message: error.message,
+ stack: error.stack,
+ url: window.location.href,
+ userAgent: navigator.userAgent,
+ tags: ["unhandledrejection"],
+ });
+ });
+
+ // 未捕获的同步错误
+ window.addEventListener("error", (event) => {
+ if (!event.message) return;
+ reportError({
+ message: event.message,
+ url: event.filename || window.location.href,
+ line: event.lineno,
+ column: event.colno,
+ userAgent: navigator.userAgent,
+ tags: ["window"],
+ });
+ });
+});
diff --git a/bun.lock b/bun.lock
index 5ed2bb7..cd5b52c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -41,6 +41,7 @@
"zod": "4.3.6",
},
"devDependencies": {
+ "@iconify-json/ph": "^1.2.2",
"@nuxt/kit": "4.4.6",
"@tailwindcss/vite": "4.3.0",
"@types/multer": "2.1.0",
@@ -237,6 +238,8 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "@iconify-json/ph": ["@iconify-json/ph@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw=="],
+
"@iconify/collections": ["@iconify/collections@1.0.689", "https://registry.npmmirror.com/@iconify/collections/-/collections-1.0.689.tgz", { "dependencies": { "@iconify/types": "*" } }, "sha512-/XdTbQnsxfvpNvZvPCI2TGZHxem40PjUZ6IXoGA4M5QugVNN+l311l4PWyxECXOt95iFoKA8ZcHzyZVWFW9OFw=="],
"@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
diff --git a/package.json b/package.json
index a702640..68f30f5 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"zod": "4.3.6"
},
"devDependencies": {
+ "@iconify-json/ph": "^1.2.2",
"@nuxt/kit": "4.4.6",
"@tailwindcss/vite": "4.3.0",
"@types/multer": "2.1.0",
diff --git a/packages/common/config/index.ts b/packages/common/config/index.ts
index 8e3a32c..cab7cd2 100644
--- a/packages/common/config/index.ts
+++ b/packages/common/config/index.ts
@@ -12,6 +12,8 @@ export const API_ALLOWLIST: RouteRule[] = [
{ path: "/api/config/global", methods: ["GET"] },
{ path: "/api/auth/oauth/:provider/callback", methods: ["GET"] },
{ path: "/api/auth/oauth/:provider/authorize", methods: ["GET"] },
+ /** 客户端错误上报 — 公开(无需登录) */
+ { path: "/api/_error-report", methods: ["POST"] },
/** 卡片列表 + 详情 */
{ path: "/api/cards", methods: ["GET"] },
{ path: "/api/cards/:id", methods: ["GET"] },
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index 1816764..832dfa3 100644
Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ
diff --git a/packages/env/schema.ts b/packages/env/schema.ts
index 6b97c1d..d411fc0 100644
--- a/packages/env/schema.ts
+++ b/packages/env/schema.ts
@@ -23,6 +23,9 @@ export const envSchema = z.object({
// ── Logging (logger package) ──
LOG_MAX_SIZE: z.coerce.number().int().positive().default(10 * 1024 * 1024), // 10 MB
LOG_MAX_BACKUPS: z.coerce.number().int().nonnegative().default(5),
+ LOG_LEVEL: z
+ .enum(["trace", "debug", "info", "warn", "error", "fatal", "mark", "off"])
+ .default("info"),
// ── Static file serving ──
STATIC_DIR: z.string().default("static"),
diff --git a/packages/logger/index.ts b/packages/logger/index.ts
index 889b703..95d6762 100644
--- a/packages/logger/index.ts
+++ b/packages/logger/index.ts
@@ -7,6 +7,7 @@ const logDir = path.resolve(process.cwd(), "logs");
const pathLog = path.resolve(logDir, "running.log");
const maxLogSize = env.LOG_MAX_SIZE;
const maxBackups = env.LOG_MAX_BACKUPS;
+const logLevel = env.LOG_LEVEL;
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
@@ -31,7 +32,7 @@ const configureLogger = () => {
categories: {
default: {
appenders: ["file", "console"],
- level: "all",
+ level: logLevel,
}
},
};
diff --git a/server/api/_error-report.post.ts b/server/api/_error-report.post.ts
new file mode 100644
index 0000000..13d56ae
--- /dev/null
+++ b/server/api/_error-report.post.ts
@@ -0,0 +1,38 @@
+import log4js from "logger";
+
+const logger = log4js.getLogger("ERROR");
+
+interface ClientErrorReport {
+ message: string;
+ stack?: string;
+ url?: string;
+ line?: number;
+ column?: number;
+ userAgent?: string;
+ tags?: string[];
+}
+
+export default defineEventHandler(async (event) => {
+ const body = await readBody