diff --git a/package.json b/package.json index bb24cbd0..a4764b99 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "sockette": "^2.0.6", "swr": "^2.2.5", "tar": "^7.4.3", - "types-pac": "^1.0.3" + "types-pac": "^1.0.3", + "zustand": "^5.0.1" }, "devDependencies": { "@actions/github": "^5.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dfd9293..56654321 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,9 @@ importers: types-pac: specifier: ^1.0.3 version: 1.0.3 + zustand: + specifier: ^5.0.1 + version: 5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) devDependencies: "@actions/github": specifier: ^5.1.1 @@ -4916,6 +4919,27 @@ packages: engines: { node: ">= 14" } hasBin: true + zustand@5.0.1: + resolution: + { + integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==, + } + engines: { node: ">=12.20.0" } + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: { @@ -8001,4 +8025,10 @@ snapshots: yaml@2.6.0: {} + zustand@5.0.1(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)): + optionalDependencies: + "@types/react": 18.3.12 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + zwitch@2.0.4: {} diff --git a/src/hooks/use-log-data.ts b/src/hooks/use-log-data.ts index 13339aa2..5957aeed 100644 --- a/src/hooks/use-log-data.ts +++ b/src/hooks/use-log-data.ts @@ -3,12 +3,20 @@ import { useEnableLog } from "../services/states"; import { createSockette } from "../utils/websocket"; import { useClashInfo } from "./use-clash"; import dayjs from "dayjs"; +import { create } from "zustand"; const MAX_LOG_NUM = 1000; -// 添加 LogLevel 类型定义 export type LogLevel = "warning" | "info" | "debug" | "error"; +// 添加 ILogItem 接口定义 +interface ILogItem { + time?: string; + type: string; + payload: string; + [key: string]: any; +} + const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => { const baseUrl = `ws://${server}/logs`; const params = new URLSearchParams(); @@ -21,12 +29,44 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => { return queryString ? `${baseUrl}?${queryString}` : baseUrl; }; +interface LogStore { + logs: Record; + clearLogs: (level?: LogLevel) => void; + appendLog: (level: LogLevel, log: ILogItem) => void; +} + +const useLogStore = create( + (set: (fn: (state: LogStore) => Partial) => void) => ({ + logs: { + warning: [], + info: [], + debug: [], + error: [], + }, + clearLogs: (level?: LogLevel) => + set((state: LogStore) => ({ + logs: level + ? { ...state.logs, [level]: [] } + : { warning: [], info: [], debug: [], error: [] }, + })), + appendLog: (level: LogLevel, log: ILogItem) => + set((state: LogStore) => { + const currentLogs = state.logs[level]; + const newLogs = + currentLogs.length >= MAX_LOG_NUM + ? [...currentLogs.slice(1), log] + : [...currentLogs, log]; + return { logs: { ...state.logs, [level]: newLogs } }; + }), + }) +); + export const useLogData = (logLevel: LogLevel) => { const { clashInfo } = useClashInfo(); - const [enableLog] = useEnableLog(); + const { logs, appendLog } = useLogStore(); - return useSWRSubscription( + useSWRSubscription( enableLog && clashInfo ? ["getClashLog", logLevel] : null, (_key, { next }) => { const { server = "", secret = "" } = clashInfo!; @@ -34,14 +74,8 @@ export const useLogData = (logLevel: LogLevel) => { const s = createSockette(buildWSUrl(server, secret, logLevel), { onmessage(event) { const data = JSON.parse(event.data) as ILogItem; - - // append new log item on socket message - next(null, (l = []) => { - const time = dayjs().format("MM-DD HH:mm:ss"); - - if (l.length >= MAX_LOG_NUM) l.shift(); - return [...l, { ...data, time }]; - }); + const time = dayjs().format("MM-DD HH:mm:ss"); + appendLog(logLevel, { ...data, time }); }, onerror(event) { this.close(); @@ -52,10 +86,13 @@ export const useLogData = (logLevel: LogLevel) => { return () => { s.close(); }; - }, - { - fallbackData: [], - keepPreviousData: true, } ); + + return logs[logLevel]; +}; + +// 导出清空日志的方法 +export const clearLogs = (level?: LogLevel) => { + useLogStore.getState().clearLogs(level); }; diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index 06518d35..b51abfe8 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -6,7 +6,7 @@ import { PlayCircleOutlineRounded, PauseCircleOutlineRounded, } from "@mui/icons-material"; -import { useLogData, LogLevel } from "@/hooks/use-log-data"; +import { useLogData, LogLevel, clearLogs } from "@/hooks/use-log-data"; import { useEnableLog } from "@/services/states"; import { BaseEmpty, BasePage } from "@/components/base"; import LogItem from "@/components/log/log-item"; @@ -22,7 +22,7 @@ const LogPage = () => { const isDark = theme.palette.mode === "dark"; const [logState, setLogState] = useState("info"); const [match, setMatch] = useState(() => (_: string) => true); - const { data: logData } = useLogData(logState); + const logData = useLogData(logState); const filterLogs = useMemo(() => { return logData @@ -56,10 +56,8 @@ const LogPage = () => {