feat(logs): support reverse chronological order #5513

Closes #5513
This commit is contained in:
Slinetrac
2025-11-18 17:39:56 +08:00
parent 4fa8b1f118
commit e7812396df
18 changed files with 103 additions and 4 deletions

View File

@@ -17,6 +17,7 @@
- 支持连接页面各个项目的排序 - 支持连接页面各个项目的排序
- 实现可选的自动备份 - 实现可选的自动备份
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接) - 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
- 日志页面支持按时间倒序
</details> </details>

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "السجلات" "title": "السجلات"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Protokolle" "title": "Protokolle"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Logs" "title": "Logs"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Registros" "title": "Registros"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "لاگ‌ها" "title": "لاگ‌ها"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Log" "title": "Log"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "ログ" "title": "ログ"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "로그" "title": "로그"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Логи" "title": "Логи"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Günlükler" "title": "Günlükler"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "Логлар" "title": "Логлар"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "日志" "title": "日志"
},
"actions": {
"showDescending": "按时间倒序",
"showAscending": "按时间正序"
} }
} }

View File

@@ -1,5 +1,9 @@
{ {
"page": { "page": {
"title": "日誌" "title": "日誌"
},
"actions": {
"showDescending": "按時間倒序",
"showAscending": "按時間正序"
} }
} }

View File

@@ -1,6 +1,7 @@
import { import {
PlayCircleOutlineRounded, PlayCircleOutlineRounded,
PauseCircleOutlineRounded, PauseCircleOutlineRounded,
SwapVertRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
@@ -20,6 +21,8 @@ const LogPage = () => {
const [clashLog, setClashLog] = useClashLog(); const [clashLog, setClashLog] = useClashLog();
const enableLog = clashLog.enable; const enableLog = clashLog.enable;
const logState = clashLog.logFilter; const logState = clashLog.logFilter;
const logOrder = clashLog.logOrder ?? "asc";
const isDescending = logOrder === "desc";
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const [searchState, setSearchState] = useState<SearchState>(); const [searchState, setSearchState] = useState<SearchState>();
@@ -49,6 +52,11 @@ const LogPage = () => {
}); });
}, [logData, logState, match]); }, [logData, logState, match]);
const filteredLogs = useMemo(
() => (isDescending ? [...filterLogs].reverse() : filterLogs),
[filterLogs, isDescending],
);
const handleLogLevelChange = (newLevel: string) => { const handleLogLevelChange = (newLevel: string) => {
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel })); setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }));
}; };
@@ -57,6 +65,13 @@ const LogPage = () => {
setClashLog((pre: any) => ({ ...pre, enable: !enableLog })); setClashLog((pre: any) => ({ ...pre, enable: !enableLog }));
}; };
const handleToggleOrder = () => {
setClashLog((pre: any) => ({
...pre,
logOrder: pre.logOrder === "desc" ? "asc" : "desc",
}));
};
return ( return (
<BasePage <BasePage
full full
@@ -86,6 +101,28 @@ const LogPage = () => {
<PlayCircleOutlineRounded /> <PlayCircleOutlineRounded />
)} )}
</IconButton> </IconButton>
<IconButton
title={t(
isDescending
? "logs.actions.showAscending"
: "logs.actions.showDescending",
)}
aria-label={t(
isDescending
? "logs.actions.showAscending"
: "logs.actions.showDescending",
)}
size="small"
color="inherit"
onClick={handleToggleOrder}
>
<SwapVertRounded
sx={{
transform: isDescending ? "scaleY(-1)" : "none",
transition: "transform 0.2s ease",
}}
/>
</IconButton>
<Button <Button
size="small" size="small"
@@ -129,17 +166,17 @@ const LogPage = () => {
/> />
</Box> </Box>
{filterLogs.length > 0 ? ( {filteredLogs.length > 0 ? (
<Virtuoso <Virtuoso
initialTopMostItemIndex={999} initialTopMostItemIndex={isDescending ? 0 : 999}
data={filterLogs} data={filteredLogs}
style={{ style={{
flex: 1, flex: 1,
}} }}
itemContent={(index, item) => ( itemContent={(index, item) => (
<LogItem value={item} searchState={searchState} /> <LogItem value={item} searchState={searchState} />
)} )}
followOutput={"smooth"} followOutput={isDescending ? false : "smooth"}
/> />
) : ( ) : (
<BaseEmpty /> <BaseEmpty />

View File

@@ -7,16 +7,19 @@ const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
>("light"); >("light");
export type LogFilter = "all" | "debug" | "info" | "warn" | "err"; export type LogFilter = "all" | "debug" | "info" | "warn" | "err";
export type LogOrder = "asc" | "desc";
interface IClashLog { interface IClashLog {
enable: boolean; enable: boolean;
logLevel: LogLevel; logLevel: LogLevel;
logFilter: LogFilter; logFilter: LogFilter;
logOrder: LogOrder;
} }
const defaultClashLog: IClashLog = { const defaultClashLog: IClashLog = {
enable: true, enable: true,
logLevel: "info", logLevel: "info",
logFilter: "all", logFilter: "all",
logOrder: "asc",
}; };
export const useClashLog = () => export const useClashLog = () =>
useLocalStorage<IClashLog>("clash-log", defaultClashLog, { useLocalStorage<IClashLog>("clash-log", defaultClashLog, {

View File

@@ -108,6 +108,8 @@ export const translationKeys = [
"layout.components.navigation.menu.unlock", "layout.components.navigation.menu.unlock",
"layout.components.navigation.menu.lock", "layout.components.navigation.menu.lock",
"logs.page.title", "logs.page.title",
"logs.actions.showDescending",
"logs.actions.showAscending",
"profiles.page.actions.updateAll", "profiles.page.actions.updateAll",
"profiles.page.actions.viewRuntimeConfig", "profiles.page.actions.viewRuntimeConfig",
"profiles.page.actions.reactivate", "profiles.page.actions.reactivate",

View File

@@ -197,6 +197,10 @@ export interface TranslationResources {
}; };
}; };
logs: { logs: {
actions: {
showAscending: string;
showDescending: string;
};
page: { page: {
title: string; title: string;
}; };