New Interface (initial commit)

This commit is contained in:
coolcoala
2025-07-04 02:28:27 +03:00
parent 4435a5aee4
commit 686490ded1
121 changed files with 12852 additions and 13274 deletions

View File

@@ -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>
);
};