New Interface (initial commit)
This commit is contained in:
@@ -1,21 +1,23 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Box, Button, IconButton, MenuItem } from "@mui/material";
|
||||
// LogPage.tsx
|
||||
|
||||
import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
|
||||
import {
|
||||
PlayCircleOutlineRounded,
|
||||
PauseCircleOutlineRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Play, Pause, Trash2, Menu } from "lucide-react";
|
||||
import { LogLevel } from "@/hooks/use-log-data";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useEnableLog } from "@/services/states";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import { BaseEmpty } from "@/components/base/base-empty";
|
||||
import LogItem from "@/components/log/log-item";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { BaseSearchBox } from "@/components/base/base-search-box";
|
||||
import { BaseStyledSelect } from "@/components/base/base-styled-select";
|
||||
import { SearchState } from "@/components/base/base-search-box";
|
||||
import {
|
||||
useGlobalLogData,
|
||||
@@ -23,13 +25,29 @@ import {
|
||||
changeLogLevel,
|
||||
toggleLogEnabled,
|
||||
} from "@/services/global-log-service";
|
||||
import { cn } from "@root/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
const LogPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [enableLog, setEnableLog] = useEnableLog();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
|
||||
"log:log-level",
|
||||
"info",
|
||||
@@ -38,13 +56,23 @@ const LogPage = () => {
|
||||
const logData = useGlobalLogData(logLevel);
|
||||
const [searchState, setSearchState] = useState<SearchState>();
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
const handleScroll = () => {
|
||||
if (scrollContainer) setIsScrolled(scrollContainer.scrollTop > 5);
|
||||
};
|
||||
scrollContainer?.addEventListener("scroll", handleScroll);
|
||||
return () => scrollContainer?.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const filterLogs = useMemo(() => {
|
||||
return logData
|
||||
? logData.filter((data) => {
|
||||
// 构建完整的搜索文本,包含时间、类型和内容
|
||||
const searchText =
|
||||
`${data.time || ""} ${data.type} ${data.payload}`.toLowerCase();
|
||||
|
||||
return logLevel === "all"
|
||||
? match(searchText)
|
||||
: data.type.toLowerCase() === logLevel && match(searchText);
|
||||
@@ -68,89 +96,116 @@ const LogPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(matcher: (content: string) => boolean, state: SearchState) => {
|
||||
setMatch(() => matcher);
|
||||
setSearchState(state);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const menuItems = [
|
||||
{ label: t("Home"), path: "/home" },
|
||||
{ label: t("Profiles"), path: "/profile" },
|
||||
{ label: t("Settings"), path: "/settings" },
|
||||
{ label: t("Proxies"), path: "/proxies" },
|
||||
{ label: t("Connections"), path: "/connections" },
|
||||
{ label: t("Rules"), path: "/rules" },
|
||||
];
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Logs")}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "auto",
|
||||
}}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
title={t(enableLog ? "Pause" : "Resume")}
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={handleToggleLog}
|
||||
>
|
||||
{enableLog ? (
|
||||
<PauseCircleOutlineRounded />
|
||||
) : (
|
||||
<PlayCircleOutlineRounded />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
{enableLog === true && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
clearGlobalLogs();
|
||||
}}
|
||||
>
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
mb: 0.5,
|
||||
mx: "10px",
|
||||
height: "39px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
<div className="h-full w-full relative">
|
||||
{/* "Липкая" шапка */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 left-0 right-0 z-10 p-4 transition-all duration-200",
|
||||
// --- НАЧАЛО ИЗМЕНЕНИЙ ---
|
||||
// Вместо блюра делаем солидный фон с тенью при прокрутке
|
||||
{ "bg-background shadow-md": isScrolled },
|
||||
// --- КОНЕЦ ИЗМЕНЕНИЙ ---
|
||||
)}
|
||||
>
|
||||
<BaseStyledSelect
|
||||
value={logLevel}
|
||||
onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="warning">WARNING</MenuItem>
|
||||
<MenuItem value="error">ERROR</MenuItem>
|
||||
<MenuItem value="debug">DEBUG</MenuItem>
|
||||
</BaseStyledSelect>
|
||||
<BaseSearchBox
|
||||
onSearch={(matcher, state) => {
|
||||
setMatch(() => matcher);
|
||||
setSearchState(state);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t("Logs")}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title={t(enableLog ? "Pause" : "Resume")}
|
||||
onClick={handleToggleLog}
|
||||
>
|
||||
{enableLog ? (
|
||||
<Pause className="h-5 w-5" />
|
||||
) : (
|
||||
<Play className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
{enableLog && (
|
||||
<Button size="sm" variant="outline" onClick={clearGlobalLogs}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" title={t("Menu")}>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("Menu")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{menuItems.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.path}
|
||||
onSelect={() => navigate(item.path)}
|
||||
disabled={location.pathname === item.path}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select value={logLevel} onValueChange={handleLogLevelChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder={t("Log Level")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">ALL</SelectItem>
|
||||
<SelectItem value="info">INFO</SelectItem>
|
||||
<SelectItem value="warning">WARNING</SelectItem>
|
||||
<SelectItem value="error">ERROR</SelectItem>
|
||||
<SelectItem value="debug">DEBUG</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex-grow">
|
||||
<BaseSearchBox onSearch={handleSearch} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
initialTopMostItemIndex={999}
|
||||
data={filterLogs}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
itemContent={(index, item) => (
|
||||
<LogItem value={item} searchState={searchState} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</BasePage>
|
||||
{/* Возвращаем Virtuoso на место */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="absolute top-0 left-0 right-0 bottom-0 pt-32 overflow-y-auto"
|
||||
>
|
||||
{filterLogs.length > 0 ? (
|
||||
<Virtuoso
|
||||
data={filterLogs}
|
||||
itemContent={(index, item) => (
|
||||
<LogItem value={item} searchState={searchState} />
|
||||
)}
|
||||
followOutput={"smooth"}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<BaseEmpty />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user