mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-19 03:49:22 +08:00
feat: 添加磨砂玻璃效果自定义配置及相关样式支持
This commit is contained in:
@@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
@@ -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" />
|
||||
|
@@ -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) : "离线"}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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: {
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
85
src/components/ui/tips.tsx
Normal file
85
src/components/ui/tips.tsx
Normal 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;
|
@@ -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)
|
||||
|
@@ -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 (
|
||||
|
@@ -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,
|
||||
};
|
||||
|
147
src/index.css
147
src/index.css
@@ -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));
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -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
|
||||
|
@@ -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
268
src/palette-rgb.css
Normal 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;
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user