149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
import { defineStore } from 'pinia';
|
||
import { ref, watch } from 'vue';
|
||
|
||
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||
|
||
export interface LogEntry {
|
||
id: number;
|
||
time: string;
|
||
level: LogLevel;
|
||
module: string;
|
||
message: string;
|
||
detail?: any;
|
||
}
|
||
|
||
const MAX_LOGS = 500;
|
||
const STORAGE_KEY = 'qz-app-logs';
|
||
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
|
||
|
||
function loadFromStorage(): LogEntry[] {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw);
|
||
if (Array.isArray(parsed)) return parsed as LogEntry[];
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
return [];
|
||
}
|
||
|
||
export const useLogStore = defineStore('log', () => {
|
||
const logs = ref<LogEntry[]>(loadFromStorage());
|
||
let nextId = logs.value.length > 0 ? Math.max(...logs.value.map((l) => l.id)) + 1 : 1;
|
||
|
||
watch(
|
||
logs,
|
||
(newLogs) => {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newLogs));
|
||
} catch {
|
||
// ignore quota issues
|
||
}
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
const add = (level: LogLevel, module: string, message: string, detail?: any) => {
|
||
const now = new Date();
|
||
const hh = now.getHours().toString().padStart(2, '0');
|
||
const mm = now.getMinutes().toString().padStart(2, '0');
|
||
const ss = now.getSeconds().toString().padStart(2, '0');
|
||
const ms = now.getMilliseconds().toString().padStart(3, '0');
|
||
|
||
// Error 对象属性不可枚举(JSON.stringify 会得到 {}),这里先规范化
|
||
let normalizedDetail = detail;
|
||
if (detail instanceof Error) {
|
||
normalizedDetail = {
|
||
name: detail.name,
|
||
message: detail.message,
|
||
stack: detail.stack,
|
||
cause: (detail as any).cause,
|
||
__isError: true,
|
||
};
|
||
} else if (detail && typeof detail === 'object') {
|
||
// 避免 Proxy 等响应式包装的干扰 — 简单浅拷贝为纯对象
|
||
try {
|
||
normalizedDetail = JSON.parse(JSON.stringify(detail, (_, v) => {
|
||
if (v instanceof Error) {
|
||
return { name: v.name, message: v.message, stack: v.stack, __isError: true };
|
||
}
|
||
return v;
|
||
}));
|
||
} catch {
|
||
normalizedDetail = String(detail);
|
||
}
|
||
}
|
||
|
||
const entry: LogEntry = {
|
||
id: nextId++,
|
||
time: `${hh}:${mm}:${ss}.${ms}`,
|
||
level,
|
||
module,
|
||
message,
|
||
detail: normalizedDetail,
|
||
};
|
||
|
||
logs.value.push(entry);
|
||
if (logs.value.length > MAX_LOGS) {
|
||
logs.value = logs.value.slice(-MAX_LOGS);
|
||
}
|
||
|
||
// 同步到浏览器控制台,方便调试
|
||
const consoleArgs = [`[${entry.time}] [${level.toUpperCase()}] [${module}]`, message];
|
||
if (detail !== undefined) consoleArgs.push(detail);
|
||
try {
|
||
if (level === 'error') console.error(...consoleArgs);
|
||
else if (level === 'warn') console.warn(...consoleArgs);
|
||
else if (level === 'debug') console.debug(...consoleArgs);
|
||
else console.log(...consoleArgs);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
};
|
||
|
||
const info = (module: string, message: string, detail?: any) => add('info', module, message, detail);
|
||
const warn = (module: string, message: string, detail?: any) => add('warn', module, message, detail);
|
||
const error = (module: string, message: string, detail?: any) => add('error', module, message, detail);
|
||
const debug = (module: string, message: string, detail?: any) => add('debug', module, message, detail);
|
||
|
||
const clear = () => {
|
||
logs.value = [];
|
||
nextId = 1;
|
||
};
|
||
|
||
const filter = (module?: string, level?: LogLevel) => {
|
||
return logs.value.filter((l) => {
|
||
if (module && l.module !== module) return false;
|
||
if (level && LEVEL_ORDER[l.level] < LEVEL_ORDER[level]) return false;
|
||
return true;
|
||
});
|
||
};
|
||
|
||
const moduleList = () => {
|
||
const set = new Set<string>();
|
||
for (const l of logs.value) set.add(l.module);
|
||
return Array.from(set).sort();
|
||
};
|
||
|
||
const countByLevel = () => {
|
||
const c = { info: 0, warn: 0, error: 0, debug: 0 };
|
||
for (const l of logs.value) c[l.level]++;
|
||
return c;
|
||
};
|
||
|
||
return {
|
||
logs,
|
||
info,
|
||
warn,
|
||
error,
|
||
debug,
|
||
add,
|
||
clear,
|
||
filter,
|
||
moduleList,
|
||
countByLevel,
|
||
};
|
||
});
|