feat: 添加磨砂玻璃效果自定义配置及相关样式支持

This commit is contained in:
Montia37
2025-08-26 03:25:50 +08:00
parent 832a4dc3d9
commit 78e02f0ca2
22 changed files with 769 additions and 225 deletions

View File

@@ -2,7 +2,7 @@
"name": "Komari Theme PurCart",
"short": "PurCarte",
"description": "A frosted glass theme for Komari",
"version": "1.0.5",
"version": "1.0.7",
"author": "Montia & Gemini",
"url": "https://github.com/Montia37/Komari-theme-purcarte",
"preview": "preview.png",
@@ -20,6 +20,20 @@
"default": "/assets/Moonlit-Scenery.webp",
"help": "目前仅支持单张背景图片eg: https://test.com/1.png"
},
{
"key": "blurValue",
"name": "磨砂玻璃模糊值",
"type": "number",
"default": 10,
"help": "调整模糊值大小,数值越大模糊效果越明显,建议值为 5-20为 0 则表示不启用模糊效果"
},
{
"key": "blurBackgroundColor",
"name": "磨砂玻璃背景色",
"type": "string",
"default": "rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)",
"help": "调整模糊背景色,推荐 rgba 颜色值,使用“|”分隔亮色模式和暗色模式的颜色值eg: rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)"
},
{
"key": "tagDefaultColorList",
"name": "标签默认颜色列表",
@@ -108,6 +122,13 @@
"default": true,
"help": "启用后默认显示分组栏"
},
{
"key": "enableSwap",
"name": "启用 SWAP 显示",
"type": "switch",
"default": true,
"help": "启用后默认显示 SWAP 信息"
},
{
"name": "Instance 设置",
"type": "title"
@@ -135,4 +156,4 @@
}
]
}
}
}

View File

@@ -2,8 +2,8 @@ import React from "react";
const Footer: React.FC = () => {
return (
<footer className="fixed shadow-inner bottom-0 left-0 right-0 p-2 text-center bg-white/10 dark:bg-gray-800/10 backdrop-blur-md border-t border-white/20 dark:border-white/10 z-50">
<p className="flex justify-center text-sm text-gray-700 dark:text-gray-200 text-shadow-lg whitespace-pre">
<footer className="fixed inset-shadow-sm bottom-0 left-0 right-0 p-2 text-center purcarte-blur z-50">
<p className="flex justify-center text-sm text-second-foreground text-shadow-lg whitespace-pre">
Powered by{" "}
<a
href="https://github.com/komari-monitor/komari"

View File

@@ -55,7 +55,7 @@ export const Header = ({
}, [sitename]);
return (
<header className="bg-background/60 backdrop-blur-[10px] border-b border-border/60 sticky top-0 flex items-center justify-center shadow-sm z-10">
<header className="purcarte-blur border-b border-border sticky top-0 flex items-center justify-center shadow-sm z-10">
<div className="w-[90%] max-w-screen-2xl px-4 py-2 flex items-center justify-between">
<div className="flex items-center text-shadow-lg text-accent-foreground">
<a href="/" className="flex items-center gap-2 text-2xl font-bold">
@@ -71,7 +71,7 @@ export const Header = ({
{isMobile ? (
<>
<div
className={`absolute top-full left-0 w-full bg-background/60 backdrop-blur-[10px] p-2 border-b border-border/60 shadow-sm z-10 transform transition-all duration-300 ease-in-out ${
className={`absolute top-full left-0 w-full purcarte-blur p-2 border-b border-border shadow-sm z-10 transform transition-all duration-300 ease-in-out ${
isSearchOpen
? "opacity-100 translate-y-0"
: "opacity-0 -translate-y-4 pointer-events-none"
@@ -106,7 +106,7 @@ export const Header = ({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="animate-in slide-in-from-top-5 duration-300 bg-background/60 backdrop-blur-[10px] border-border/60 rounded-xl">
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-border rounded-xl">
<DropdownMenuItem
onClick={() =>
setViewMode(viewMode === "grid" ? "table" : "grid")
@@ -216,7 +216,7 @@ export const Header = ({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="animate-in slide-in-from-top-5 duration-300 bg-background/60 backdrop-blur-[10px] border-border/60 rounded-xl">
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-border rounded-xl">
<DropdownMenuItem onClick={toggleTheme}>
{theme === "dark" ? (
<Sun className="size-4 mr-2 text-primary" />

View File

@@ -16,9 +16,10 @@ import { CircleProgress } from "../ui/circle-progress";
interface NodeCardProps {
node: NodeWithStatus;
enableSwap: boolean | undefined;
}
export const NodeCard = ({ node }: NodeCardProps) => {
export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
const {
stats,
isOnline,
@@ -40,7 +41,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
return (
<Card
className={`flex flex-col mx-auto bg-card backdrop-blur-xs w-full min-w-[280px] max-w-sm ${
className={`flex flex-col mx-auto purcarte-blur w-full min-w-[280px] max-w-sm ${
isOnline
? ""
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
@@ -104,7 +105,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
<span className="w-12 text-right">{memUsage.toFixed(0)}%</span>
</div>
</div>
{node.swap_total > 0 ? (
{enableSwap && (
<div className="flex items-center justify-between">
<span className="text-secondary-foreground">SWAP</span>
<div className="w-3/4 flex items-center gap-2">
@@ -112,15 +113,11 @@ export const NodeCard = ({ node }: NodeCardProps) => {
value={swapUsage}
className={getProgressBarClass(swapUsage)}
/>
<span className="w-12 text-right">{swapUsage.toFixed(0)}%</span>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<span className="text-secondary-foreground">SWAP</span>
<div className="w-3/4 flex items-center gap-2">
<ProgressBar value={0} />
<span className="w-12 text-right">OFF</span>
{node.swap_total > 0 ? (
<span className="w-12 text-right">{swapUsage.toFixed(0)}%</span>
) : (
<span className="w-12 text-right">OFF</span>
)}
</div>
</div>
)}
@@ -188,7 +185,7 @@ export const NodeCard = ({ node }: NodeCardProps) => {
<div className="flex items-center gap-1">{expired_at}</div>
</div>
<div className="border-l border-border/60 mx-2"></div>
<div className="flex justify-start w-full">
<div className="flex justify-end w-full">
<span className="text-secondary-foreground">线</span>
<span>
{isOnline && stats ? formatUptime(stats.uptime) : "离线"}

View File

@@ -1,10 +1,16 @@
export const NodeListHeader = () => {
interface NodeListHeaderProps {
enableSwap: boolean | undefined;
}
export const NodeListHeader = ({ enableSwap }: NodeListHeaderProps) => {
const gridCols = enableSwap ? "grid-cols-10" : "grid-cols-9";
return (
<div className="text-primary font-bold grid grid-cols-10 text-center shadow-md gap-4 p-2 items-center rounded-lg bg-card/50 transition-colors duration-200">
<div
className={`text-primary font-bold grid ${gridCols} text-center shadow-md gap-4 p-2 items-center rounded-lg bg-card transition-colors duration-200`}>
<div className="col-span-2"></div>
<div className="col-span-1">CPU</div>
<div className="col-span-1"></div>
<div className="col-span-1">SWAP</div>
{enableSwap && <div className="col-span-1">SWAP</div>}
<div className="col-span-1"></div>
<div className="col-span-1"></div>
<div className="col-span-2"></div>

View File

@@ -9,9 +9,10 @@ import { CircleProgress } from "../ui/circle-progress";
interface NodeListItemProps {
node: NodeWithStatus;
enableSwap: boolean | undefined;
}
export const NodeListItem = ({ node }: NodeListItemProps) => {
export const NodeListItem = ({ node, enableSwap }: NodeListItemProps) => {
const {
stats,
isOnline,
@@ -25,9 +26,11 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
trafficPercentage,
} = useNodeCommons(node);
const gridCols = enableSwap ? "grid-cols-10" : "grid-cols-9";
return (
<div
className={`grid grid-cols-10 text-center shadow-md gap-4 p-2 text-nowrap items-center rounded-lg ${
className={`grid ${gridCols} text-center shadow-md gap-4 p-2 text-nowrap items-center rounded-lg ${
isOnline
? ""
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
@@ -39,53 +42,56 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
<div className="text-base font-bold">{node.name}</div>
<Tag className="text-xs" tags={tagList} />
<div className="flex text-xs">
<div className="flex">
<span className="text-secondary-foreground"></span>
<div className="flex items-center gap-1">{expired_at}</div>
</div>
<div className="border-l border-border/60 mx-2"></div>
<div className="flex">
<span className="text-secondary-foreground">线</span>
<span>
{isOnline && stats ? formatUptime(stats.uptime) : "离线"}
</span>
</div>
<span className="text-secondary-foreground"></span>
<div className="flex items-center gap-1">{expired_at}</div>
</div>
<div className="flex text-xs">
<span className="text-secondary-foreground">线</span>
<span>
{isOnline && stats ? formatUptime(stats.uptime) : "离线"}
</span>
</div>
</div>
</Link>
</div>
<div className="col-span-1">
<div className="gap-1 flex items-center justify-center whitespace-nowrap">
<CpuIcon className="inline-block size-4 flex-shrink-0 text-blue-600" />
{node.cpu_cores} Cores
</div>
<div className="mt-1">
{isOnline ? `${cpuUsage.toFixed(1)}%` : "N/A"}
<div className="col-span-1 flex items-center text-left">
<CpuIcon className="inline-block size-5 flex-shrink-0 text-blue-600" />
<div className="ml-1 w-full items-center justify-center">
<div>{node.cpu_cores} Cores</div>
<div>{isOnline ? `${cpuUsage.toFixed(1)}%` : "N/A"}</div>
</div>
</div>
<div className="col-span-1">
<div className="gap-1 flex items-center justify-center whitespace-nowrap">
<MemoryStickIcon className="inline-block size-4 flex-shrink-0 text-green-600" />
{formatBytes(node.mem_total)}
</div>
<div className="mt-1">
{isOnline ? `${memUsage.toFixed(1)}%` : "N/A"}
<div className="col-span-1 flex items-center text-left">
<MemoryStickIcon className="inline-block size-5 flex-shrink-0 text-green-600" />
<div className="ml-1 w-full items-center justify-center">
<div>{formatBytes(node.mem_total)}</div>
<div className="mt-1">
{isOnline ? `${memUsage.toFixed(1)}%` : "N/A"}
</div>
</div>
</div>
{node.swap_total > 0 ? (
<div className="col-span-1">
{isOnline ? `${swapUsage.toFixed(1)}%` : "N/A"}
{enableSwap && (
<div className="col-span-1 flex items-center text-left">
<MemoryStickIcon className="inline-block size-5 flex-shrink-0 text-purple-600" />
{node.swap_total > 0 ? (
<div className="ml-1 w-full items-center justify-center">
<div>{formatBytes(node.swap_total)}</div>
<div className="mt-1">
{isOnline ? `${swapUsage.toFixed(1)}%` : "N/A"}
</div>
</div>
) : (
<div className="ml-1 w-full item-center justify-center">OFF</div>
)}
</div>
) : (
<div className="col-span-1 text-secondary-foreground">OFF</div>
)}
<div className="col-span-1">
<div className="gap-1 flex items-center justify-center whitespace-nowrap">
<HardDriveIcon className="inline-block size-4 flex-shrink-0 text-red-600" />
{formatBytes(node.disk_total)}
</div>
<div className="mt-1">
{isOnline ? `${diskUsage.toFixed(1)}%` : "N/A"}
<div className="col-span-1 flex items-center text-left">
<HardDriveIcon className="inline-block size-5 flex-shrink-0 text-red-600" />
<div className="ml-1 w-full items-center justify-center">
<div>{formatBytes(node.disk_total)}</div>
<div className="mt-1">
{isOnline ? `${diskUsage.toFixed(1)}%` : "N/A"}
</div>
</div>
</div>
<div className="col-span-1">
@@ -95,7 +101,7 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
<div className="col-span-2">
<div className="flex items-center justify-around">
{node.traffic_limit !== 0 && isOnline && stats && (
<div className="flex items-center justify-center w-1/4">
<div className="flex items-center justify-center w-1/3">
<CircleProgress
value={trafficPercentage}
maxValue={100}
@@ -106,14 +112,12 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
</div>
)}
<div
className={node.traffic_limit !== 0 ? "w-3/4 text-left" : "w-full"}>
className={node.traffic_limit !== 0 ? "w-2/3 text-left" : "w-full"}>
<div>
<span>
{stats ? formatBytes(stats.network.totalUp) : "N/A"}
</span>
<span className="ml-2">
<div> {stats ? formatBytes(stats.network.totalUp) : "N/A"}</div>
<div>
{stats ? formatBytes(stats.network.totalDown) : "N/A"}
</span>
</div>
</div>
{node.traffic_limit !== 0 && isOnline && stats && (
<div>
@@ -127,7 +131,9 @@ export const NodeListItem = ({ node }: NodeListItemProps) => {
</div>
</div>
<div className="col-span-1">
<span>{load}</span>
{load.split("|").map((item, index) => (
<div key={index}>{item.trim()}</div>
))}
</div>
</div>
);

View File

@@ -149,7 +149,7 @@ export const StatsBar = ({
}
};
return (
<div className="bg-card backdrop-blur-[10px] min-w-[300px] rounded-lg box-border border text-secondary-foreground my-6 mx-4 px-4 md:text-base text-sm relative flex items-center min-h-[5rem]">
<div className="purcarte-blur min-w-[300px] rounded-lg text-secondary-foreground my-6 mx-4 px-4 box-border border border-border text-sm relative flex items-center min-h-[5rem]">
<div className="absolute top-2 right-2">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>

View File

@@ -10,15 +10,15 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
"bg-primary/60 text-primary-foreground shadow-xs hover:bg-primary/80",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive/60 text-white shadow-xs hover:bg-destructive/80 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background/60 shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary/60 text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"hover:bg-accent/80 hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {

View File

@@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-xl border bg-card backdrop-blur-xs text-card-foreground shadow",
"rounded-xl purcarte-blur text-card-foreground shadow-sm box-border border border-border",
className
)}
{...props}

View File

@@ -43,7 +43,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
@@ -61,7 +61,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}

View File

@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-(--accent-track) data-[state=unchecked]:bg-input",
className
)}
{...props}

View File

@@ -0,0 +1,85 @@
import React, { useState } from "react";
import { Info } from "lucide-react";
import { Popover, Dialog } from "@radix-ui/themes";
import { useIsMobile } from "@/hooks/useMobile";
interface TipsProps {
size?: string;
color?: string;
children?: React.ReactNode;
trigger?: React.ReactNode;
mode?: "popup" | "dialog" | "auto";
side?: "top" | "right" | "bottom" | "left";
}
const Tips: React.FC<TipsProps & React.HTMLAttributes<HTMLDivElement>> = ({
size = "16",
color = "gray",
trigger,
children,
side = "bottom",
mode = "popup",
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const isMobile = useIsMobile();
// determine whether to render a Dialog instead of a Popover
const isDialog = mode === "dialog" || (mode === "auto" && isMobile);
const handleInteraction = () => {
// toggle when using Dialog (click) or on mobile (click)
if (isDialog || isMobile) {
setIsOpen(!isOpen);
}
};
return (
<div className="relative inline-block" {...props}>
{isDialog ? (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger>
<div
className={`flex items-center justify-center rounded-full font-bold cursor-pointer `}
onClick={handleInteraction}>
{trigger ?? <Info color={color} size={size} />}
</div>
</Dialog.Trigger>
<Dialog.Content>
<div className="flex flex-col gap-2">
{/* <label className="text-xl font-bold">Tips</label> */}
<div>{children}</div>
</div>
</Dialog.Content>
</Dialog.Root>
) : (
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger>
<div
className={`flex items-center justify-center rounded-full font-bold cursor-pointer `}
onClick={isMobile ? handleInteraction : undefined}
onMouseEnter={!isMobile ? () => setIsOpen(true) : undefined}
onMouseLeave={!isMobile ? () => setIsOpen(false) : undefined}>
{trigger ?? <Info color={color} size={size} />}
</div>
</Popover.Trigger>
<Popover.Content
side={side}
sideOffset={5}
onMouseEnter={!isMobile ? () => setIsOpen(true) : undefined}
onMouseLeave={!isMobile ? () => setIsOpen(false) : undefined}
className="purcarte-blur border border-border shadow-md rounded-md z-50 text-muted-foreground"
style={{
minWidth: isMobile ? "12rem" : "16rem",
maxWidth: isMobile ? "80vw" : "16rem",
backgroundColor: "var(--card)",
}}>
<div className="relative text-sm">{children}</div>
</Popover.Content>
</Popover.Root>
)}
</div>
);
};
export default Tips;

View File

@@ -28,7 +28,7 @@ export const CustomTooltip = ({
if (active && payload && payload.length) {
return (
<div className="bg-background/80 p-3 border rounded-lg shadow-lg max-w-xs">
<div className="bg-background/80 p-3 border border-border rounded-lg shadow-lg max-w-xs">
<p className="text-xs font-medium text-muted-foreground mb-2">
{labelFormatter
? labelFormatter(label)

View File

@@ -25,8 +25,21 @@ export function ConfigProvider({
return theme.backgroundImage || "";
}, [theme.backgroundImage]);
// 背景切换逻辑
// 使用 useMemo 缓存模糊值,避免每次渲染时重新计算
const blurValue = useMemo(() => {
return theme.blurValue ?? DEFAULT_CONFIG.blurValue ?? 10;
}, [theme.blurValue]);
// 使用 useMemo 缓存模糊背景颜色,避免每次渲染时重新计算
const blurBackgroundColor = useMemo(() => {
return (
theme.blurBackgroundColor || DEFAULT_CONFIG.blurBackgroundColor || ""
);
}, [theme.blurBackgroundColor]);
// 合并的样式设置逻辑
useEffect(() => {
// 设置背景图片
if (backgroundImage) {
document.body.style.setProperty(
"--body-background-url",
@@ -35,7 +48,30 @@ export function ConfigProvider({
} else {
document.body.style.removeProperty("--body-background-url");
}
}, [backgroundImage]);
// 设置模糊值
document.documentElement.style.setProperty(
"--purcarte-blur",
`${blurValue}px`
);
// 设置模糊背景颜色(亮色/暗色模式)
if (blurBackgroundColor) {
// 解析颜色字符串,支持逗号分隔的亮色,暗色
const colors = blurBackgroundColor
.split("|")
.map((color) => color.trim());
if (colors.length >= 2) {
// 第一个颜色用于亮色模式,第二个颜色用于暗色模式
document.documentElement.style.setProperty("--card-light", colors[0]);
document.documentElement.style.setProperty("--card-dark", colors[1]);
} else if (colors.length === 1) {
// 只有一个颜色,同时用于亮色和暗色模式
document.documentElement.style.setProperty("--card-light", colors[0]);
document.documentElement.style.setProperty("--card-dark", colors[0]);
}
}
}, [backgroundImage, blurValue, blurBackgroundColor]);
const config: ConfigOptions = useMemo(
() => ({
@@ -64,8 +100,17 @@ export function ConfigProvider({
pingChartMaxPoints:
theme.pingChartMaxPoints || DEFAULT_CONFIG.pingChartMaxPoints,
backgroundImage,
blurValue,
blurBackgroundColor,
enableSwap: theme.enableSwap ?? DEFAULT_CONFIG.enableSwap,
}),
[theme, backgroundImage, publicSettings?.sitename]
[
theme,
backgroundImage,
blurValue,
blurBackgroundColor,
publicSettings?.sitename,
]
);
return (

View File

@@ -1,6 +1,8 @@
// 配置类型定义
export interface ConfigOptions {
backgroundImage?: string; // 背景图片URL
blurValue?: number; // 磨砂玻璃模糊值
blurBackgroundColor?: string; // 磨砂玻璃背景颜色
tagDefaultColorList?: string; // 标签默认颜色列表
enableLogo?: boolean; // 是否启用Logo
logoUrl?: string; // Logo图片URL
@@ -15,11 +17,14 @@ export interface ConfigOptions {
enableInstanceDetail?: boolean; // 是否启用实例详情
enablePingChart?: boolean; // 是否启用延迟图表
pingChartMaxPoints?: number; // 延迟图表最大点数
enableSwap?: boolean; // 是否启用SWAP显示
}
// 默认配置值
export const DEFAULT_CONFIG: ConfigOptions = {
backgroundImage: "/assets/Moonlit-Scenery.webp",
blurValue: 10,
blurBackgroundColor: "rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)",
tagDefaultColorList:
"ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red,crimson,pink,plum,purple,violet,iris,indigo,blue,cyan,teal,jade,green,grass,lime,mint,sky",
enableLogo: false,
@@ -35,4 +40,5 @@ export const DEFAULT_CONFIG: ConfigOptions = {
enableInstanceDetail: true,
enablePingChart: true,
pingChartMaxPoints: 0,
enableSwap: true,
};

View File

@@ -1,5 +1,7 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "tailwindcss/theme";
@import "./palette-rgb.css";
@import "tailwindcss/preflight";
@import "tailwindcss/utilities";
@custom-variant dark (&:is(.dark *));
@@ -43,84 +45,78 @@
:root {
--radius: 0.625rem;
--background: oklch(0.985 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0 / 0.5);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.55 0.25 27);
--destructive-transparent: oklch(0.55 0.25 27 / 0.1);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: #fafafa;
--foreground: #0a0a0a;
--card: var(--card-light, #ffffff80);
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #171717;
--primary-foreground: #fafafa;
--secondary: #f5f5f5;
--secondary-foreground: #171717;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #171717;
--destructive: #df0000;
--destructive-transparent: #df00001a;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #a1a1a1;
--chart-1: #f54900;
--chart-2: #009689;
--chart-3: #104e64;
--chart-4: #ffb900;
--chart-5: #fe9a00;
--sidebar: #fafafa;
--sidebar-foreground: #0a0a0a;
--sidebar-primary: #171717;
--sidebar-primary-foreground: #fafafa;
--sidebar-accent: #f5f5f5;
--sidebar-accent-foreground: #171717;
--sidebar-border: #e5e5e5;
--sidebar-ring: #a1a1a1;
/* Frosted Glass Variables */
--frosted-bg-light: rgba(255, 255, 255, 0.1);
--frosted-border-light: rgba(255, 255, 255, 0.2);
--purcarte-blur: 10px;
--body-background-url: url("");
/* --body-background-transition: background-image 0.8s ease-in-out; */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0 / 0.5);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.8 0.15 20);
--destructive-transparent: oklch(0.8 0.15 20 / 0.25);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
/* Frosted Glass Variables */
--frosted-bg-dark: rgba(0, 0, 0, 0.1);
--frosted-border-dark: rgba(0, 0, 0, 0.2);
--background: #0a0a0a;
--foreground: #fafafa;
--card: var(--card-dark, #17171780);
--card-foreground: #fafafa;
--popover: #171717;
--popover-foreground: #fafafa;
--primary: #e5e5e5;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a1a1a1;
--accent: #262626;
--accent-foreground: #fafafa;
--destructive: #ff9395;
--destructive-transparent: #ff939540;
--border: #ffffff1a;
--input: #ffffff26;
--ring: #737373;
--chart-1: #1447e6;
--chart-2: #00bc7d;
--chart-3: #fe9a00;
--chart-4: #ad46ff;
--chart-5: #ff2056;
--sidebar: #171717;
--sidebar-foreground: #fafafa;
--sidebar-primary: #1447e6;
--sidebar-primary-foreground: #fafafa;
--sidebar-accent: #262626;
--sidebar-accent-foreground: #fafafa;
--sidebar-border: #ffffff1a;
--sidebar-ring: #737373;
}
@layer base {
@@ -161,3 +157,8 @@ body::before {
);
background-size: 60px 60px;
}
.purcarte-blur {
background: var(--color-card);
backdrop-filter: blur(var(--purcarte-blur));
}

View File

@@ -8,7 +8,7 @@ import Loading from "@/components/loading";
import type { NodeWithStatus } from "@/types/node";
import { useNodeData } from "@/contexts/NodeDataContext";
import { useLiveData } from "@/contexts/LiveDataContext";
import { useConfigItem } from "@/config";
import { useAppConfig } from "@/config";
interface HomePageProps {
viewMode: "grid" | "table";
@@ -19,8 +19,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
const { nodes: staticNodes, loading, getGroups } = useNodeData();
const { liveData } = useLiveData();
const [selectedGroup, setSelectedGroup] = useState("所有");
const enableGroupedBar = useConfigItem("enableGroupedBar");
const enableStatsBar = useConfigItem("enableStatsBar");
const { enableGroupedBar, enableStatsBar, enableSwap } = useAppConfig();
const [displayOptions, setDisplayOptions] = useState({
time: true,
online: true,
@@ -95,7 +94,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
<main className="flex-1 px-4 pb-4">
{enableGroupedBar && (
<div className="flex overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground box-border border space-x-4 px-4 rounded-lg mb-4 bg-card backdrop-blur-[10px]">
<div className="flex overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground box-border border border-border space-x-4 px-4 rounded-lg mb-4 purcarte-blur">
<span></span>
{groups.map((group: string) => (
<Button
@@ -117,7 +116,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
className={
viewMode === "grid"
? ""
: "space-y-2 bg-card overflow-auto backdrop-blur-[10px] rounded-lg p-2"
: "space-y-2 overflow-auto box-border border border-border purcarte-blur rounded-lg p-2"
}>
<div
className={
@@ -125,12 +124,22 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
? "grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4"
: "min-w-[1080px]"
}>
{viewMode === "table" && <NodeListHeader />}
{viewMode === "table" && (
<NodeListHeader enableSwap={enableSwap} />
)}
{filteredNodes.map((node: NodeWithStatus) =>
viewMode === "grid" ? (
<NodeCard key={node.uuid} node={node} />
<NodeCard
key={node.uuid}
node={node}
enableSwap={enableSwap}
/>
) : (
<NodeListItem key={node.uuid} node={node} />
<NodeListItem
key={node.uuid}
node={node}
enableSwap={enableSwap}
/>
)
)}
</div>

View File

@@ -97,14 +97,14 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
? `${formatBytes(liveData.ram.used)} / ${formatBytes(
node?.mem_total || 0
)}`
: "-"}
: "N/A"}
</label>
<label>
{liveData?.swap?.used
? `${formatBytes(liveData.swap.used)} / ${formatBytes(
node?.swap_total || 0
)}`
: "-"}
: "N/A"}
</label>
</Flex>
),
@@ -199,6 +199,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
tooltipLabel: "UDP 连接",
},
],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}` : "",
data: chartData,
},
{
@@ -208,6 +210,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
value: liveData?.process || "-",
dataKey: "process",
color: colors[0],
yAxisFormatter: (value: number, index: number) =>
index !== 0 ? `${value}` : "",
data: chartData,
tooltipLabel: "进程数",
},
@@ -260,8 +264,8 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tick={{ fontSize: 10 }}
axisLine={{ stroke: "var(--muted-foreground)" }}
tick={{ fill: "var(--muted-foreground)" }}
tickFormatter={timeFormatter}
interval={0}
height={20}
@@ -273,8 +277,11 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
tickFormatter={config.yAxisFormatter}
orientation="left"
type="number"
tick={{ fontSize: 10, dx: -8 }}
width={25}
tick={{
dx: -8,
fill: "var(--muted-foreground)",
}}
width={200}
mirror={true}
/>
<Tooltip
@@ -319,12 +326,12 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
return (
<div className="relative">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
<div className="absolute inset-0 flex items-center justify-center purcarte-blur rounded-lg z-10">
<Loading text="正在加载图表数据..." />
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
<div className="absolute inset-0 flex items-center justify-center purcarte-blur rounded-lg z-10">
<p className="text-red-500">{error}</p>
</div>
)}

View File

@@ -8,6 +8,7 @@ import {
Tooltip,
ResponsiveContainer,
Brush,
ReferenceLine,
} from "recharts";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
@@ -18,6 +19,7 @@ import { usePingChart } from "@/hooks/usePingChart";
import fillMissingTimePoints, { cutPeakValues } from "@/utils/RecordHelper";
import { useConfigItem } from "@/config";
import { CustomTooltip } from "@/components/ui/tooltip";
import Tips from "@/components/ui/tips";
interface PingChartProps {
node: NodeData;
@@ -29,6 +31,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
const [visiblePingTasks, setVisiblePingTasks] = useState<number[]>([]);
const [timeRange, setTimeRange] = useState<[number, number] | null>(null);
const [cutPeak, setCutPeak] = useState(false);
const [connectBreaks, setConnectBreaks] = useState(false);
const maxPointsToRender = useConfigItem("pingChartMaxPoints") || 0; // 0表示不限制
useEffect(() => {
@@ -144,70 +147,103 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
}, [pingHistory?.tasks]);
const generateColor = useCallback(
(taskName: string, total: number) => {
(taskName: string, total: number, isBreakPoints?: boolean) => {
const index = sortedTasks.findIndex((t) => t.name === taskName);
if (index === -1) return "#000000"; // Fallback color
const hue = (index * (360 / total)) % 360;
return `hsl(${hue}, 50%, 60%)`;
return `hsla(${hue}, 50%, 60%, ${isBreakPoints ? 0.7 : 1})`;
},
[sortedTasks]
);
const breakPoints = useMemo(() => {
if (!connectBreaks || !chartData || chartData.length < 2) {
return [];
}
const points: { x: number; color: string }[] = [];
for (const task of sortedTasks) {
if (!visiblePingTasks.includes(task.id)) {
continue;
}
const taskKey = String(task.id);
for (let i = 1; i < chartData.length; i++) {
const prevPoint = chartData[i - 1];
const currentPoint = chartData[i];
const isBreak =
(currentPoint[taskKey] === null ||
currentPoint[taskKey] === undefined) &&
prevPoint[taskKey] !== null &&
prevPoint[taskKey] !== undefined;
if (isBreak) {
points.push({
x: currentPoint.time,
color: generateColor(task.name, sortedTasks.length, true),
});
}
}
}
return points;
}, [chartData, sortedTasks, visiblePingTasks, generateColor, connectBreaks]);
return (
<div className="relative space-y-4">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
<div className="absolute inset-0 flex items-center justify-center purcarte-blur rounded-lg z-10">
<Loading text="正在加载图表数据..." />
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-card/50 backdrop-blur-sm rounded-lg z-10">
<div className="absolute inset-0 flex items-center justify-center purcarte-blur rounded-lg z-10">
<p className="text-red-500">{error}</p>
</div>
)}
<Card>
<CardContent className="p-2">
<div className="flex flex-wrap gap-2 items-center justify-center">
{sortedTasks.map((task) => {
const values = chartData
.map((d) => d[task.id])
.filter((v) => v !== null && v !== undefined) as number[];
const loss =
chartData.length > 0
? (1 - values.length / chartData.length) * 100
: 0;
const min = values.length > 0 ? Math.min(...values) : 0;
const isVisible = visiblePingTasks.includes(task.id);
const color = generateColor(task.name, sortedTasks.length);
{pingHistory?.tasks && pingHistory.tasks.length > 0 && (
<Card>
<CardContent className="p-2">
<div className="flex flex-wrap gap-2 items-center justify-center">
{sortedTasks.map((task) => {
const values = chartData
.map((d) => d[task.id])
.filter((v) => v !== null && v !== undefined) as number[];
const loss =
chartData.length > 0
? (1 - values.length / chartData.length) * 100
: 0;
const min = values.length > 0 ? Math.min(...values) : 0;
const isVisible = visiblePingTasks.includes(task.id);
const color = generateColor(task.name, sortedTasks.length);
return (
<div
key={task.id}
className={`h-auto px-3 py-1.5 flex flex-col leading-snug text-center cursor-pointer rounded-md transition-all outline-2 outline ${
isVisible ? "" : "outline-transparent"
}`}
onClick={() => handleTaskVisibilityToggle(task.id)}
style={{
outlineColor: isVisible ? color : undefined,
boxShadow: isVisible ? `0 0 8px ${color}` : undefined,
}}>
<div className="font-semibold">{task.name}</div>
<span className="text-xs font-normal">
{loss.toFixed(1)}% | {min.toFixed(0)}ms
</span>
</div>
);
})}
</div>
</CardContent>
</Card>
return (
<div
key={task.id}
className={`h-auto px-3 py-1.5 flex flex-col leading-snug text-center cursor-pointer rounded-md transition-all outline-2 outline ${
isVisible ? "" : "outline-transparent"
}`}
onClick={() => handleTaskVisibilityToggle(task.id)}
style={{
outlineColor: isVisible ? color : undefined,
boxShadow: isVisible ? `0 0 8px ${color}` : undefined,
}}>
<div className="font-semibold">{task.name}</div>
<span className="text-xs font-normal">
{loss.toFixed(1)}% | {min.toFixed(0)}ms
</span>
</div>
);
})}
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-2">
<Switch
id="peak-shaving"
@@ -215,6 +251,30 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
onCheckedChange={setCutPeak}
/>
<Label htmlFor="peak-shaving"></Label>
<Tips>
<span
dangerouslySetInnerHTML={{
__html:
'<h2 class="text-lg font-bold">关于数据平滑的提示</h2><p>当您开启平滑后,您在统计图中看到的曲线经过<strong>指数加权移动平均 (EWMA)</strong> 算法处理,这是一种常用的数据平滑技术。</p></br><p>需要注意的是经过EWMA算法平滑后的曲线所展示的数值<strong>并非原始的、真实的测量数据</strong>。它们是根据EWMA算法计算得出的一个<strong>平滑趋势线</strong>,旨在减少数据波动,使数据模式和趋势更容易被识别。</p></br><p>因此,您看到的数值更像是<strong>视觉上的呈现</strong>,帮助您更好地理解数据的整体走向和长期趋势,而不是每一个时间点的精确真实值。如果您需要查看具体、原始的数据点,请参考未经平滑处理的数据视图。</p>',
}}
/>
</Tips>
</div>
<div className="flex items-center space-x-2">
<Switch
id="connect-breaks"
checked={connectBreaks}
onCheckedChange={setConnectBreaks}
/>
<Label htmlFor="connect-breaks"></Label>
<Tips>
<span
dangerouslySetInnerHTML={{
__html:
'<h2 class="text-lg font-bold">关于连接断点的提示</h2><p>当您开启"连接断点"功能后,图表中的曲线将会跨过那些由于网络问题或其他原因导致的丢包点,形成一条连续的线条。同时,系统会在丢包位置显示<strong>半透明的垂直参考线</strong>来标记断点位置。</p>',
}}
/>
</Tips>
</div>
</div>
</div>
@@ -244,24 +304,39 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
minute: "2-digit",
});
}}
tick={{ fill: "var(--muted-foreground)" }}
scale="time"
/>
<YAxis mirror={true} width={30} />
<YAxis
mirror={true}
width={30}
tick={{ fill: "var(--muted-foreground)" }}
/>
<Tooltip
cursor={false}
content={<CustomTooltip labelFormatter={lableFormatter} />}
/>
{connectBreaks &&
breakPoints.map((point, index) => (
<ReferenceLine
key={`break-${index}`}
x={point.x}
stroke={point.color}
strokeWidth={1}
strokeOpacity={0.5}
/>
))}
{sortedTasks.map((task) => (
<Line
key={task.id}
type={cutPeak ? "basis" : "linear"}
type={"basis"}
dataKey={String(task.id)}
name={task.name}
stroke={generateColor(task.name, sortedTasks.length)}
strokeWidth={2}
hide={!visiblePingTasks.includes(task.id)}
dot={false}
connectNulls={false}
connectNulls={connectBreaks}
/>
))}
<Brush

View File

@@ -11,6 +11,7 @@ const PingChart = lazy(() => import("./PingChart"));
import Loading from "@/components/loading";
import Flag from "@/components/sections/Flag";
import { useConfigItem } from "@/config";
import { useIsMobile } from "@/hooks/useMobile";
const InstancePage = () => {
const { uuid } = useParams<{ uuid: string }>();
@@ -28,6 +29,7 @@ const InstancePage = () => {
const [pingHours, setPingHours] = useState<number>(1); // 默认1小时
const enableInstanceDetail = useConfigItem("enableInstanceDetail");
const enablePingChart = useConfigItem("enablePingChart");
const isMobile = useIsMobile();
const maxRecordPreserveTime = publicSettings?.record_preserve_time || 0; // 默认0表示关闭
const maxPingRecordPreserveTime =
@@ -106,10 +108,10 @@ const InstancePage = () => {
return (
<div className="w-[90%] max-w-screen-2xl mx-auto flex-1 flex flex-col pb-15 p-4 space-y-4">
<div className="flex items-center justify-between bg-card box-border border rounded-lg p-4 mb-4 text-secondary-foreground">
<div className="flex items-center justify-between purcarte-blur box-border border border-border rounded-lg p-4 mb-4 text-secondary-foreground">
<div className="flex items-center gap-2 min-w-0">
<Button
className="bg-card flex-shrink-0"
className="flex-shrink-0"
variant="ghost"
size="icon"
onClick={() => navigate(-1)}>
@@ -127,24 +129,31 @@ const InstancePage = () => {
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
<div className="flex justify-center w-full">
<div className="bg-card border rounded-lg py-3 px-4">
<div className="flex flex-col items-center w-full space-y-4">
<div className="purcarte-blur box-border border border-border rounded-lg p-2">
<div className="flex justify-center space-x-2">
<Button
variant={chartType === "load" ? "secondary" : "ghost"}
size="sm"
onClick={() => setChartType("load")}>
</Button>
{enablePingChart && (
<Button
variant={chartType === "ping" ? "secondary" : "ghost"}
size="sm"
onClick={() => setChartType("ping")}>
</Button>
)}
</div>
</div>
<div
className={`purcarte-blur box-border border border-border justify-center rounded-lg p-2 ${
isMobile ? "w-full" : ""
}`}>
{chartType === "load" ? (
<div className="flex justify-center space-x-2 mt-2">
<div className="flex space-x-2 overflow-x-auto whitespace-nowrap">
{loadTimeRanges.map((range) => (
<Button
key={range.label}
@@ -156,7 +165,7 @@ const InstancePage = () => {
))}
</div>
) : (
<div className="flex justify-center space-x-2 mt-2">
<div className="flex space-x-2 overflow-x-auto whitespace-nowrap">
{pingTimeRanges.map((range) => (
<Button
key={range.label}

268
src/palette-rgb.css Normal file
View File

@@ -0,0 +1,268 @@
@theme {
--color-red-50: rgb(254, 242, 242);
--color-red-100: rgb(255, 226, 226);
--color-red-200: rgb(255, 201, 201);
--color-red-300: rgb(255, 162, 162);
--color-red-400: rgb(255, 100, 103);
--color-red-500: rgb(251, 44, 54);
--color-red-600: rgb(231, 0, 11);
--color-red-700: rgb(193, 0, 7);
--color-red-800: rgb(159, 7, 18);
--color-red-900: rgb(130, 24, 26);
--color-red-950: rgb(70, 8, 9);
--color-orange-50: rgb(255, 247, 237);
--color-orange-100: rgb(255, 237, 212);
--color-orange-200: rgb(255, 214, 167);
--color-orange-300: rgb(255, 184, 106);
--color-orange-400: rgb(255, 137, 4);
--color-orange-500: rgb(255, 105, 0);
--color-orange-600: rgb(245, 73, 0);
--color-orange-700: rgb(202, 53, 0);
--color-orange-800: rgb(159, 45, 0);
--color-orange-900: rgb(126, 42, 12);
--color-orange-950: rgb(68, 19, 6);
--color-amber-50: rgb(255, 251, 235);
--color-amber-100: rgb(254, 243, 198);
--color-amber-200: rgb(254, 230, 133);
--color-amber-300: rgb(255, 210, 48);
--color-amber-400: rgb(255, 185, 0);
--color-amber-500: rgb(254, 154, 0);
--color-amber-600: rgb(225, 113, 0);
--color-amber-700: rgb(187, 77, 0);
--color-amber-800: rgb(151, 60, 0);
--color-amber-900: rgb(123, 51, 6);
--color-amber-950: rgb(70, 25, 1);
--color-yellow-50: rgb(254, 252, 232);
--color-yellow-100: rgb(254, 249, 194);
--color-yellow-200: rgb(255, 240, 133);
--color-yellow-300: rgb(255, 223, 32);
--color-yellow-400: rgb(253, 199, 0);
--color-yellow-500: rgb(240, 177, 0);
--color-yellow-600: rgb(208, 135, 0);
--color-yellow-700: rgb(166, 95, 0);
--color-yellow-800: rgb(137, 75, 0);
--color-yellow-900: rgb(115, 62, 10);
--color-yellow-950: rgb(67, 32, 4);
--color-lime-50: rgb(247, 254, 231);
--color-lime-100: rgb(236, 252, 202);
--color-lime-200: rgb(216, 249, 153);
--color-lime-300: rgb(187, 244, 81);
--color-lime-400: rgb(154, 230, 0);
--color-lime-500: rgb(124, 207, 0);
--color-lime-600: rgb(94, 165, 0);
--color-lime-700: rgb(73, 125, 0);
--color-lime-800: rgb(60, 99, 0);
--color-lime-900: rgb(53, 83, 14);
--color-lime-950: rgb(25, 46, 3);
--color-green-50: rgb(240, 253, 244);
--color-green-100: rgb(220, 252, 231);
--color-green-200: rgb(185, 248, 207);
--color-green-300: rgb(123, 241, 168);
--color-green-400: rgb(5, 223, 114);
--color-green-500: rgb(0, 201, 80);
--color-green-600: rgb(0, 166, 62);
--color-green-700: rgb(0, 130, 54);
--color-green-800: rgb(1, 102, 48);
--color-green-900: rgb(13, 84, 43);
--color-green-950: rgb(3, 46, 21);
--color-emerald-50: rgb(236, 253, 245);
--color-emerald-100: rgb(208, 250, 229);
--color-emerald-200: rgb(164, 244, 207);
--color-emerald-300: rgb(94, 233, 181);
--color-emerald-400: rgb(0, 212, 146);
--color-emerald-500: rgb(0, 188, 125);
--color-emerald-600: rgb(0, 153, 102);
--color-emerald-700: rgb(0, 122, 85);
--color-emerald-800: rgb(0, 96, 69);
--color-emerald-900: rgb(0, 79, 59);
--color-emerald-950: rgb(0, 44, 34);
--color-teal-50: rgb(240, 253, 250);
--color-teal-100: rgb(203, 251, 241);
--color-teal-200: rgb(150, 247, 228);
--color-teal-300: rgb(70, 236, 213);
--color-teal-400: rgb(0, 213, 190);
--color-teal-500: rgb(0, 187, 167);
--color-teal-600: rgb(0, 150, 137);
--color-teal-700: rgb(0, 120, 111);
--color-teal-800: rgb(0, 95, 90);
--color-teal-900: rgb(11, 79, 74);
--color-teal-950: rgb(2, 47, 46);
--color-cyan-50: rgb(236, 254, 255);
--color-cyan-100: rgb(206, 250, 254);
--color-cyan-200: rgb(162, 244, 253);
--color-cyan-300: rgb(83, 234, 253);
--color-cyan-400: rgb(0, 211, 242);
--color-cyan-500: rgb(0, 184, 219);
--color-cyan-600: rgb(0, 146, 184);
--color-cyan-700: rgb(0, 117, 149);
--color-cyan-800: rgb(0, 95, 120);
--color-cyan-900: rgb(16, 78, 100);
--color-cyan-950: rgb(5, 51, 69);
--color-sky-50: rgb(240, 249, 255);
--color-sky-100: rgb(223, 242, 254);
--color-sky-200: rgb(184, 230, 254);
--color-sky-300: rgb(116, 212, 255);
--color-sky-400: rgb(0, 188, 255);
--color-sky-500: rgb(0, 166, 244);
--color-sky-600: rgb(0, 132, 209);
--color-sky-700: rgb(0, 105, 168);
--color-sky-800: rgb(0, 89, 138);
--color-sky-900: rgb(2, 74, 112);
--color-sky-950: rgb(5, 47, 74);
--color-blue-50: rgb(239, 246, 255);
--color-blue-100: rgb(219, 234, 254);
--color-blue-200: rgb(190, 219, 255);
--color-blue-300: rgb(142, 197, 255);
--color-blue-400: rgb(81, 162, 255);
--color-blue-500: rgb(43, 127, 255);
--color-blue-600: rgb(21, 93, 252);
--color-blue-700: rgb(20, 71, 230);
--color-blue-800: rgb(25, 60, 184);
--color-blue-900: rgb(28, 57, 142);
--color-blue-950: rgb(22, 36, 86);
--color-indigo-50: rgb(238, 242, 255);
--color-indigo-100: rgb(224, 231, 255);
--color-indigo-200: rgb(198, 210, 255);
--color-indigo-300: rgb(163, 179, 255);
--color-indigo-400: rgb(124, 134, 255);
--color-indigo-500: rgb(97, 95, 255);
--color-indigo-600: rgb(79, 57, 246);
--color-indigo-700: rgb(67, 45, 215);
--color-indigo-800: rgb(55, 42, 172);
--color-indigo-900: rgb(49, 44, 133);
--color-indigo-950: rgb(30, 26, 77);
--color-violet-50: rgb(245, 243, 255);
--color-violet-100: rgb(237, 233, 254);
--color-violet-200: rgb(221, 214, 255);
--color-violet-300: rgb(196, 180, 255);
--color-violet-400: rgb(166, 132, 255);
--color-violet-500: rgb(142, 81, 255);
--color-violet-600: rgb(127, 34, 254);
--color-violet-700: rgb(112, 8, 231);
--color-violet-800: rgb(93, 14, 192);
--color-violet-900: rgb(77, 23, 154);
--color-violet-950: rgb(47, 13, 104);
--color-purple-50: rgb(250, 245, 255);
--color-purple-100: rgb(243, 232, 255);
--color-purple-200: rgb(233, 212, 255);
--color-purple-300: rgb(218, 178, 255);
--color-purple-400: rgb(194, 122, 255);
--color-purple-500: rgb(173, 70, 255);
--color-purple-600: rgb(152, 16, 250);
--color-purple-700: rgb(130, 0, 219);
--color-purple-800: rgb(110, 17, 176);
--color-purple-900: rgb(89, 22, 139);
--color-purple-950: rgb(60, 3, 102);
--color-fuchsia-50: rgb(253, 244, 255);
--color-fuchsia-100: rgb(250, 232, 255);
--color-fuchsia-200: rgb(246, 207, 255);
--color-fuchsia-300: rgb(244, 168, 255);
--color-fuchsia-400: rgb(237, 106, 255);
--color-fuchsia-500: rgb(225, 42, 251);
--color-fuchsia-600: rgb(200, 0, 222);
--color-fuchsia-700: rgb(168, 0, 183);
--color-fuchsia-800: rgb(138, 1, 148);
--color-fuchsia-900: rgb(114, 19, 120);
--color-fuchsia-950: rgb(75, 0, 79);
--color-pink-50: rgb(253, 242, 248);
--color-pink-100: rgb(252, 231, 243);
--color-pink-200: rgb(252, 206, 232);
--color-pink-300: rgb(253, 165, 213);
--color-pink-400: rgb(251, 100, 182);
--color-pink-500: rgb(246, 51, 154);
--color-pink-600: rgb(230, 0, 118);
--color-pink-700: rgb(198, 0, 92);
--color-pink-800: rgb(163, 0, 76);
--color-pink-900: rgb(134, 16, 67);
--color-pink-950: rgb(81, 4, 36);
--color-rose-50: rgb(255, 241, 242);
--color-rose-100: rgb(255, 228, 230);
--color-rose-200: rgb(255, 204, 211);
--color-rose-300: rgb(255, 161, 173);
--color-rose-400: rgb(255, 99, 126);
--color-rose-500: rgb(255, 32, 86);
--color-rose-600: rgb(236, 0, 63);
--color-rose-700: rgb(199, 0, 54);
--color-rose-800: rgb(165, 0, 54);
--color-rose-900: rgb(139, 8, 54);
--color-rose-950: rgb(77, 2, 24);
--color-slate-50: rgb(248, 250, 252);
--color-slate-100: rgb(241, 245, 249);
--color-slate-200: rgb(226, 232, 240);
--color-slate-300: rgb(202, 213, 226);
--color-slate-400: rgb(144, 161, 185);
--color-slate-500: rgb(98, 116, 142);
--color-slate-600: rgb(69, 85, 108);
--color-slate-700: rgb(49, 65, 88);
--color-slate-800: rgb(29, 41, 61);
--color-slate-900: rgb(15, 23, 43);
--color-slate-950: rgb(2, 6, 24);
--color-gray-50: rgb(249, 250, 251);
--color-gray-100: rgb(243, 244, 246);
--color-gray-200: rgb(229, 231, 235);
--color-gray-300: rgb(209, 213, 220);
--color-gray-400: rgb(153, 161, 175);
--color-gray-500: rgb(106, 114, 130);
--color-gray-600: rgb(74, 85, 101);
--color-gray-700: rgb(54, 65, 83);
--color-gray-800: rgb(30, 41, 57);
--color-gray-900: rgb(16, 24, 40);
--color-gray-950: rgb(3, 7, 18);
--color-zinc-50: rgb(250, 250, 250);
--color-zinc-100: rgb(244, 244, 245);
--color-zinc-200: rgb(228, 228, 231);
--color-zinc-300: rgb(212, 212, 216);
--color-zinc-400: rgb(159, 159, 169);
--color-zinc-500: rgb(113, 113, 123);
--color-zinc-600: rgb(82, 82, 92);
--color-zinc-700: rgb(63, 63, 70);
--color-zinc-800: rgb(39, 39, 42);
--color-zinc-900: rgb(24, 24, 27);
--color-zinc-950: rgb(9, 9, 11);
--color-neutral-50: rgb(250, 250, 250);
--color-neutral-100: rgb(245, 245, 245);
--color-neutral-200: rgb(229, 229, 229);
--color-neutral-300: rgb(212, 212, 212);
--color-neutral-400: rgb(161, 161, 161);
--color-neutral-500: rgb(115, 115, 115);
--color-neutral-600: rgb(82, 82, 82);
--color-neutral-700: rgb(64, 64, 64);
--color-neutral-800: rgb(38, 38, 38);
--color-neutral-900: rgb(23, 23, 23);
--color-neutral-950: rgb(10, 10, 10);
--color-stone-50: rgb(250, 250, 249);
--color-stone-100: rgb(245, 245, 244);
--color-stone-200: rgb(231, 229, 228);
--color-stone-300: rgb(214, 211, 209);
--color-stone-400: rgb(166, 160, 155);
--color-stone-500: rgb(121, 113, 107);
--color-stone-600: rgb(87, 83, 77);
--color-stone-700: rgb(68, 64, 59);
--color-stone-800: rgb(41, 37, 36);
--color-stone-900: rgb(28, 25, 23);
--color-stone-950: rgb(12, 10, 9);
--color-black: #000;
--color-white: #fff;
}

View File

@@ -13,8 +13,17 @@ export const formatBytes = (bytes: number, isSpeed = false, decimals = 2) => {
const sizes = isSpeed
? ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"]
: ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
let i = Math.floor(Math.log(bytes) / Math.log(k));
let value = bytes / Math.pow(k, i);
// 如果值大于等于1000则进位到下一个单位
if (value >= 1000 && i < sizes.length - 1) {
i++;
value = bytes / Math.pow(k, i);
}
return parseFloat(value.toFixed(dm)) + " " + sizes[i];
};
// Helper function to format uptime