mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-19 11:59:21 +08:00
perf: 优化主题色配置以及暗色模式的文本辨识度
This commit is contained in:
@@ -2,8 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className="fixed inset-shadow-sm inset-shadow-(color:--accent-a4) bottom-0 left-0 right-0 p-2 text-center purcarte-blur z-50">
|
<footer className="fixed inset-shadow-sm inset-shadow-(color:--accent-4)/50 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 text-shadow-(color:--accent-a4) whitespace-pre">
|
<p className="flex justify-center text-sm text-secondary-foreground theme-text-shadow whitespace-pre">
|
||||||
Powered by{" "}
|
Powered by{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/komari-monitor/komari"
|
href="https://github.com/komari-monitor/komari"
|
||||||
|
@@ -57,9 +57,9 @@ export const Header = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="purcarte-blur border-b border-(--accent-a4) sticky top-0 flex items-center justify-center shadow-sm shadow-(color:--accent-a4) z-10">
|
<header className="purcarte-blur border-b border-(--accent-4)/50 sticky top-0 flex items-center justify-center shadow-sm shadow-(color:--accent-4)/50 z-10">
|
||||||
<div className="w-[90%] max-w-screen-2xl px-4 py-2 flex items-center justify-between">
|
<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-shadow-(color:--accent-a4) text-accent-foreground">
|
<div className="flex items-center theme-text-shadow text-accent-foreground">
|
||||||
<a href="/" className="flex items-center gap-2 text-2xl font-bold">
|
<a href="/" className="flex items-center gap-2 text-2xl font-bold">
|
||||||
{enableLogo && logoUrl && (
|
{enableLogo && logoUrl && (
|
||||||
<img src={logoUrl} alt="logo" className="h-8" />
|
<img src={logoUrl} alt="logo" className="h-8" />
|
||||||
@@ -87,7 +87,7 @@ export const Header = ({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="purcarte-blur border-(--accent-a4) rounded-xl w-[90vw] translate-x-[5vw] mt-[.5rem] max-w-screen-2xl">
|
className="purcarte-blur border-(--accent-4)/50 rounded-xl w-[90vw] translate-x-[5vw] mt-[.5rem] max-w-screen-2xl">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -114,7 +114,7 @@ export const Header = ({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="purcarte-blur mt-[.5rem] border-(--accent-a4) rounded-xl">
|
className="purcarte-blur mt-[.5rem] border-(--accent-4)/50 rounded-xl">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewMode(viewMode === "grid" ? "table" : "grid")
|
setViewMode(viewMode === "grid" ? "table" : "grid")
|
||||||
@@ -231,7 +231,7 @@ export const Header = ({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-(--accent-a4) rounded-xl">
|
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-(--accent-4)/50 rounded-xl">
|
||||||
<DropdownMenuItem onClick={toggleAppearance}>
|
<DropdownMenuItem onClick={toggleAppearance}>
|
||||||
{appearance === "dark" ? (
|
{appearance === "dark" ? (
|
||||||
<Sun className="size-4 mr-2 text-primary" />
|
<Sun className="size-4 mr-2 text-primary" />
|
||||||
|
@@ -12,7 +12,7 @@ import Flag from "./Flag";
|
|||||||
import { Tag } from "../ui/tag";
|
import { Tag } from "../ui/tag";
|
||||||
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
||||||
import { ProgressBar } from "../ui/progress-bar";
|
import { ProgressBar } from "../ui/progress-bar";
|
||||||
import { CircleProgress } from "../ui/circle-progress";
|
import { CircleProgress } from "../ui/progress-circle";
|
||||||
|
|
||||||
interface NodeCardProps {
|
interface NodeCardProps {
|
||||||
node: NodeWithStatus;
|
node: NodeWithStatus;
|
||||||
@@ -58,7 +58,7 @@ export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
|
|||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Tag tags={tagList} />
|
<Tag tags={tagList} />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-(--accent-a4) my-2"></div>
|
<div className="border-t border-(--accent-4)/50 my-2"></div>
|
||||||
<div className="flex items-center justify-around whitespace-nowrap">
|
<div className="flex items-center justify-around whitespace-nowrap">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<CpuIcon className="size-4 text-blue-600 flex-shrink-0" />
|
<CpuIcon className="size-4 text-blue-600 flex-shrink-0" />
|
||||||
@@ -113,7 +113,7 @@ export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
|
|||||||
<span className="w-12 text-right">{diskUsage.toFixed(0)}%</span>
|
<span className="w-12 text-right">{diskUsage.toFixed(0)}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-(--accent-a4) my-2"></div>
|
<div className="border-t border-(--accent-4)/50 my-2"></div>
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-secondary-foreground">网络:</span>
|
<span className="text-secondary-foreground">网络:</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -167,7 +167,7 @@ export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
|
|||||||
到期:{expired_at}
|
到期:{expired_at}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l border-(--accent-a4) mx-2"></div>
|
<div className="border-l border-(--accent-4)/50 mx-2"></div>
|
||||||
<div className="flex justify-end w-full">
|
<div className="flex justify-end w-full">
|
||||||
<span className="text-secondary-foreground">
|
<span className="text-secondary-foreground">
|
||||||
{isOnline && stats
|
{isOnline && stats
|
||||||
|
@@ -6,7 +6,7 @@ export const NodeListHeader = ({ enableSwap }: NodeListHeaderProps) => {
|
|||||||
const gridCols = enableSwap ? "grid-cols-10" : "grid-cols-9";
|
const gridCols = enableSwap ? "grid-cols-10" : "grid-cols-9";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`text-primary font-bold grid ${gridCols} text-center shadow-sm shadow-(color:--accent-a4) gap-4 p-2 items-center rounded-lg bg-card transition-colors duration-200`}>
|
className={`text-primary font-bold grid ${gridCols} text-center shadow-sm shadow-(color:--accent-4)/50 gap-4 p-2 items-center rounded-lg bg-card transition-colors duration-200`}>
|
||||||
<div className="col-span-2">节点名称</div>
|
<div className="col-span-2">节点名称</div>
|
||||||
<div className="col-span-1">CPU</div>
|
<div className="col-span-1">CPU</div>
|
||||||
<div className="col-span-1">内存</div>
|
<div className="col-span-1">内存</div>
|
||||||
|
@@ -5,7 +5,7 @@ import { CpuIcon, MemoryStickIcon, HardDriveIcon } from "lucide-react";
|
|||||||
import Flag from "./Flag";
|
import Flag from "./Flag";
|
||||||
import { Tag } from "../ui/tag";
|
import { Tag } from "../ui/tag";
|
||||||
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
import { useNodeCommons } from "@/hooks/useNodeCommons";
|
||||||
import { CircleProgress } from "../ui/circle-progress";
|
import { CircleProgress } from "../ui/progress-circle";
|
||||||
import { ProgressBar } from "../ui/progress-bar";
|
import { ProgressBar } from "../ui/progress-bar";
|
||||||
|
|
||||||
interface NodeListItemProps {
|
interface NodeListItemProps {
|
||||||
@@ -36,7 +36,7 @@ export const NodeListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid ${gridCols} text-center shadow-sm shadow-(color:--accent-a4) gap-4 p-2 text-nowrap items-center rounded-lg ${
|
className={`grid ${gridCols} text-center shadow-sm shadow-(color:--accent-4)/50 gap-4 p-2 text-nowrap items-center rounded-lg ${
|
||||||
isOnline
|
isOnline
|
||||||
? ""
|
? ""
|
||||||
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
|
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"
|
||||||
|
@@ -156,7 +156,7 @@ export const StatsBar = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="purcarte-blur min-w-[300px] rounded-lg text-secondary-foreground my-6 mx-4 px-4 box-border border border-(--accent-a4) text-sm relative flex items-center min-h-[5rem]">
|
<div className="purcarte-blur min-w-[300px] text-secondary-foreground my-6 mx-4 px-4 theme-card-style text-sm relative flex items-center min-h-[5rem]">
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 right-2">
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
@@ -10,14 +10,14 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-(--accent-a5) text-primary-foreground shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6)",
|
"bg-(--accent-5)/50 text-primary shadow-sm shadow-(color:--accent-4)/50 hover:bg-(--accent-6)/50",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-(--accent-a5) text-white shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6) focus-visible:ring-destructive/20",
|
"bg-(--accent-5)/50 text-white shadow-sm shadow-(color:--accent-4)/50 hover:bg-(--accent-6)/50 focus-visible:ring-destructive/20",
|
||||||
outline:
|
outline:
|
||||||
"border border-(--accent-a6) bg-(--accent-a5) shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6) hover:text-accent-foreground",
|
"bg-(--accent-5)/50 theme-card-style hover:bg-(--accent-6)/50 hover:text-accent-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-(--accent-a5) text-secondary-foreground shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6)",
|
"bg-(--accent-5)/50 text-secondary-foreground shadow-sm shadow-(color:--accent-4)/50 hover:bg-(--accent-6)/50",
|
||||||
ghost: "hover:bg-(--accent-a5) hover:bg-(--accent-a6)",
|
ghost: "hover:bg-(--accent-5)/50 hover:bg-(--accent-6)/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl purcarte-blur text-card-foreground shadow-sm shadow-(color:--accent-a4) box-border border border-(--accent-a4)",
|
"purcarte-blur theme-card-style text-card-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -177,7 +177,7 @@ function ChartTooltipContent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-(--accent-a4) bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
"border-(--accent-4)/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
{!nestLabel ? tooltipLabel : null}
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
@@ -45,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-(--accent-a4) bg-popover p-1 text-popover-foreground shadow-md",
|
"z-50 min-w-[8rem] purcarte-blur overflow-hidden theme-card-style bg-popover p-1 text-popover-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -64,7 +64,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-(--accent-a4) bg-popover p-1 text-popover-foreground shadow-md",
|
"z-50 min-w-[8rem] purcarte-blur overflow-hidden theme-card-style bg-popover p-1 text-popover-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -9,7 +9,7 @@ export const ProgressBar = ({
|
|||||||
h?: string;
|
h?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div className={`w-full bg-gray-200 rounded-full ${h} dark:bg-gray-700`}>
|
<div className={`w-full bg-(--accent-4)/50 rounded-full ${h}`}>
|
||||||
<div
|
<div
|
||||||
className={`${h} rounded-full transition-all duration-500 ${getProgressBarClass(
|
className={`${h} rounded-full transition-all duration-500 ${getProgressBarClass(
|
||||||
value
|
value
|
||||||
|
@@ -39,7 +39,7 @@ export const CircleProgress: React.FC<CircleProgressProps> = ({
|
|||||||
cx={size / 2}
|
cx={size / 2}
|
||||||
cy={size / 2}
|
cy={size / 2}
|
||||||
r={radius}
|
r={radius}
|
||||||
className="stroke-gray-200 dark:stroke-gray-700"
|
className="stroke-(--accent-4)/50"
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
@@ -1,25 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
||||||
|
|
||||||
import { cn } from "@/utils";
|
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
||||||
>(({ className, value, ...props }, ref) => (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
));
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
|
||||||
|
|
||||||
export { Progress };
|
|
@@ -11,14 +11,14 @@ const Switch = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
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-(--accent-a8) data-[state=unchecked]:bg-(--accent-a4)",
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-(--accent-5) 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-8)/50 data-[state=unchecked]:bg-(--accent-4)/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}>
|
ref={ref}>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
"pointer-events-none block h-5 w-5 rounded-full bg-(--accent-9) dark:bg-(--accent-5) shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
|
@@ -89,7 +89,7 @@ const Tag = React.forwardRef<HTMLDivElement, TagProps>(
|
|||||||
<Badge
|
<Badge
|
||||||
key={index}
|
key={index}
|
||||||
color={badgeColor as ColorType}
|
color={badgeColor as ColorType}
|
||||||
variant="soft"
|
variant="surface"
|
||||||
className="text-sm">
|
className="text-sm">
|
||||||
<label className="text-xs">{text}</label>
|
<label className="text-xs">{text}</label>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
@@ -68,7 +68,7 @@ const Tips: React.FC<TipsProps & React.HTMLAttributes<HTMLDivElement>> = ({
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
onMouseEnter={!isMobile ? () => setIsOpen(true) : undefined}
|
onMouseEnter={!isMobile ? () => setIsOpen(true) : undefined}
|
||||||
onMouseLeave={!isMobile ? () => setIsOpen(false) : undefined}
|
onMouseLeave={!isMobile ? () => setIsOpen(false) : undefined}
|
||||||
className="purcarte-blur border border-(--accent-a4) shadow-md rounded-md z-50 text-muted-foreground"
|
className="purcarte-blur theme-card-style z-50 text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
minWidth: isMobile ? "12rem" : "16rem",
|
minWidth: isMobile ? "12rem" : "16rem",
|
||||||
maxWidth: isMobile ? "80vw" : "16rem",
|
maxWidth: isMobile ? "80vw" : "16rem",
|
||||||
|
@@ -28,7 +28,7 @@ export const CustomTooltip = ({
|
|||||||
|
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/80 p-3 border border-(--accent-a4) rounded-lg shadow-lg max-w-xs">
|
<div className="bg-background/80 p-3 theme-card-style max-w-xs">
|
||||||
<p className="text-xs font-medium text-muted-foreground mb-2">
|
<p className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
{labelFormatter
|
{labelFormatter
|
||||||
? labelFormatter(label)
|
? labelFormatter(label)
|
||||||
|
@@ -79,9 +79,7 @@
|
|||||||
--sidebar-ring: #a1a1a1;
|
--sidebar-ring: #a1a1a1;
|
||||||
|
|
||||||
--purcarte-blur: 10px;
|
--purcarte-blur: 10px;
|
||||||
|
|
||||||
--body-background-url: url("");
|
--body-background-url: url("");
|
||||||
/* --body-background-transition: background-image 0.8s ease-in-out; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -121,7 +119,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-(--accent-a4) outline-ring/50;
|
@apply border-(--accent-4)/50 outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@@ -131,6 +129,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.radix-themes {
|
||||||
|
--theme-text-muted-color: rgb(from var(--accent-4) r g b / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-text-shadow {
|
||||||
|
@apply text-shadow-sm text-shadow-(color:--accent-8)/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-card-style {
|
||||||
|
@apply rounded-lg shadow-sm shadow-(color:--accent-4)/50 box-border border border-(--accent-4)/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-text-muted {
|
||||||
|
@apply text-(--theme-text-muted-color) text-shadow-sm text-shadow-(color:--accent-foreground)/20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 背景图片伪元素 */
|
/* 背景图片伪元素 */
|
||||||
body::before {
|
body::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
@@ -73,7 +73,11 @@ export const AppContent = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
<HomePage viewMode={viewMode} searchTerm={searchTerm} />
|
<HomePage
|
||||||
|
viewMode={viewMode}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
setSearchTerm={setSearchTerm}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/instance/:uuid" element={<InstancePage />} />
|
<Route path="/instance/:uuid" element={<InstancePage />} />
|
||||||
|
@@ -9,10 +9,18 @@ import type { NodeWithStatus } from "@/types/node";
|
|||||||
import { useNodeData } from "@/contexts/NodeDataContext";
|
import { useNodeData } from "@/contexts/NodeDataContext";
|
||||||
import { useLiveData } from "@/contexts/LiveDataContext";
|
import { useLiveData } from "@/contexts/LiveDataContext";
|
||||||
import { useAppConfig } from "@/config";
|
import { useAppConfig } from "@/config";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
viewMode: "grid" | "table";
|
viewMode: "grid" | "table";
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
setSearchTerm: (term: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeStateCache = {
|
const homeStateCache = {
|
||||||
@@ -20,7 +28,11 @@ const homeStateCache = {
|
|||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
|
const HomePage: React.FC<HomePageProps> = ({
|
||||||
|
viewMode,
|
||||||
|
searchTerm,
|
||||||
|
setSearchTerm,
|
||||||
|
}) => {
|
||||||
const { nodes: staticNodes, loading, getGroups } = useNodeData();
|
const { nodes: staticNodes, loading, getGroups } = useNodeData();
|
||||||
const { liveData } = useLiveData();
|
const { liveData } = useLiveData();
|
||||||
const [selectedGroup, setSelectedGroup] = useState(
|
const [selectedGroup, setSelectedGroup] = useState(
|
||||||
@@ -133,7 +145,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
|
|||||||
|
|
||||||
<main className="flex-1 px-4 pb-4">
|
<main className="flex-1 px-4 pb-4">
|
||||||
{enableGroupedBar && (
|
{enableGroupedBar && (
|
||||||
<div className="flex overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground box-border border border-(--accent-a4) space-x-4 px-4 rounded-lg mb-4 purcarte-blur">
|
<div className="flex purcarte-blur theme-card-style overflow-auto whitespace-nowrap overflow-x-auto items-center min-w-[300px] text-secondary-foreground space-x-4 px-4 mb-4">
|
||||||
<span>分组</span>
|
<span>分组</span>
|
||||||
{groups.map((group: string) => (
|
{groups.map((group: string) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -155,7 +167,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
|
|||||||
className={
|
className={
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? ""
|
? ""
|
||||||
: "space-y-2 overflow-auto box-border border border-(--accent-a4) purcarte-blur rounded-lg p-2"
|
: "space-y-2 overflow-auto purcarte-blur theme-card-style p-2"
|
||||||
}>
|
}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -185,9 +197,20 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12">
|
<div className="flex flex-grow items-center justify-center">
|
||||||
<p className="text-lg font-bold">没有结果</p>
|
<Card className="w-full max-w-md">
|
||||||
<p className="text-muted-foreground">请尝试更改筛选条件</p>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">
|
||||||
|
Not Found
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>请尝试更改筛选条件</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter>
|
||||||
|
<Button onClick={() => setSearchTerm("")} className="w-full">
|
||||||
|
清空搜索
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import type { NodeWithStatus } from "@/types/node";
|
import type { NodeWithStatus } from "@/types/node";
|
||||||
import { useMemo, memo } from "react";
|
import { useMemo, memo } from "react";
|
||||||
import { formatBytes, formatUptime, formatTrafficLimit } from "@/utils";
|
import { formatBytes, formatUptime, formatTrafficLimit } from "@/utils";
|
||||||
import { CircleProgress } from "@/components/ui/circle-progress";
|
import { CircleProgress } from "@/components/ui/progress-circle";
|
||||||
|
|
||||||
interface InstanceProps {
|
interface InstanceProps {
|
||||||
node: NodeWithStatus;
|
node: NodeWithStatus;
|
||||||
@@ -50,27 +50,27 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
<CardContent className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<p className="text-muted-foreground">CPU</p>
|
<p className="theme-text-muted">CPU</p>
|
||||||
<p>{`${node.cpu_name} (x${node.cpu_cores})`}</p>
|
<p>{`${node.cpu_name} (x${node.cpu_cores})`}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">架构</p>
|
<p className="theme-text-muted">架构</p>
|
||||||
<p>{node.arch}</p>
|
<p>{node.arch}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">虚拟化</p>
|
<p className="theme-text-muted">虚拟化</p>
|
||||||
<p>{node.virtualization}</p>
|
<p>{node.virtualization}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">GPU</p>
|
<p className="theme-text-muted">GPU</p>
|
||||||
<p>{node.gpu_name || "N/A"}</p>
|
<p>{node.gpu_name || "N/A"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">操作系统</p>
|
<p className="theme-text-muted">操作系统</p>
|
||||||
<p>{node.os}</p>
|
<p>{node.os}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">内存</p>
|
<p className="theme-text-muted">内存</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? `${formatBytes(stats.ram.used)} / ${formatBytes(
|
? `${formatBytes(stats.ram.used)} / ${formatBytes(
|
||||||
@@ -80,7 +80,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">交换</p>
|
<p className="theme-text-muted">交换</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? `${formatBytes(stats.swap.used)} / ${formatBytes(
|
? `${formatBytes(stats.swap.used)} / ${formatBytes(
|
||||||
@@ -90,7 +90,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">磁盘</p>
|
<p className="theme-text-muted">磁盘</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? `${formatBytes(stats.disk.used)} / ${formatBytes(
|
? `${formatBytes(stats.disk.used)} / ${formatBytes(
|
||||||
@@ -100,11 +100,11 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">运行时间</p>
|
<p className="theme-text-muted">运行时间</p>
|
||||||
<p>{formatUptime(stats?.uptime || 0)}</p>
|
<p>{formatUptime(stats?.uptime || 0)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">实时网络</p>
|
<p className="theme-text-muted">实时网络</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? `↑ ${formatBytes(stats.network.up, true)} ↓ ${formatBytes(
|
? `↑ ${formatBytes(stats.network.up, true)} ↓ ${formatBytes(
|
||||||
@@ -115,7 +115,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">总流量</p>
|
<p className="theme-text-muted">总流量</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{node.traffic_limit !== 0 && isOnline && stats && (
|
{node.traffic_limit !== 0 && isOnline && stats && (
|
||||||
<CircleProgress
|
<CircleProgress
|
||||||
@@ -144,7 +144,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">负载</p>
|
<p className="theme-text-muted">负载</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? `${stats.load.load1.toFixed(2)} | ${stats.load.load5.toFixed(
|
? `${stats.load.load1.toFixed(2)} | ${stats.load.load5.toFixed(
|
||||||
@@ -154,7 +154,7 @@ const Instance = memo(({ node }: InstanceProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">最后上报</p>
|
<p className="theme-text-muted">最后上报</p>
|
||||||
<p>
|
<p>
|
||||||
{stats && isOnline
|
{stats && isOnline
|
||||||
? new Date(stats.updated_at).toLocaleString()
|
? new Date(stats.updated_at).toLocaleString()
|
||||||
|
@@ -264,8 +264,12 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={{ stroke: "var(--muted-foreground)" }}
|
axisLine={{
|
||||||
tick={{ fill: "var(--muted-foreground)" }}
|
stroke: "var(--theme-text-muted-color) !important",
|
||||||
|
}}
|
||||||
|
tick={{
|
||||||
|
fill: "var(--theme-text-muted-color) !important",
|
||||||
|
}}
|
||||||
tickFormatter={timeFormatter}
|
tickFormatter={timeFormatter}
|
||||||
interval={0}
|
interval={0}
|
||||||
height={20}
|
height={20}
|
||||||
@@ -279,7 +283,7 @@ const LoadCharts = memo(({ node, hours, liveData }: LoadChartsProps) => {
|
|||||||
type="number"
|
type="number"
|
||||||
tick={{
|
tick={{
|
||||||
dx: -8,
|
dx: -8,
|
||||||
fill: "var(--muted-foreground)",
|
fill: "var(--theme-text-muted-color) !important",
|
||||||
}}
|
}}
|
||||||
width={200}
|
width={200}
|
||||||
mirror={true}
|
mirror={true}
|
||||||
|
@@ -356,13 +356,13 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
tick={{ fill: "var(--muted-foreground)" }}
|
tick={{ fill: "var(--theme-text-muted-color)" }}
|
||||||
scale="time"
|
scale="time"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
mirror={true}
|
mirror={true}
|
||||||
width={30}
|
width={30}
|
||||||
tick={{ fill: "var(--muted-foreground)" }}
|
tick={{ fill: "var(--theme-text-muted-color)" }}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
cursor={false}
|
cursor={false}
|
||||||
@@ -395,7 +395,7 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
dataKey="time"
|
dataKey="time"
|
||||||
height={30}
|
height={30}
|
||||||
stroke="var(--accent-track)"
|
stroke="var(--accent-track)"
|
||||||
fill="transparent"
|
fill="var(--accent-4)"
|
||||||
alwaysShowText
|
alwaysShowText
|
||||||
tickFormatter={(time) => {
|
tickFormatter={(time) => {
|
||||||
const date = new Date(time);
|
const date = new Date(time);
|
||||||
|
@@ -107,8 +107,8 @@ const InstancePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="w-[90%] max-w-screen-2xl text-card-foreground mx-auto flex-1 flex flex-col pb-15 p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between purcarte-blur box-border border border-(--accent-a4) rounded-lg p-4 mb-4 text-secondary-foreground">
|
<div className="flex items-center justify-between purcarte-blur theme-card-style p-4 mb-4 text-secondary-foreground">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<Button
|
<Button
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
@@ -130,7 +130,7 @@ const InstancePage = () => {
|
|||||||
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
|
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
|
||||||
|
|
||||||
<div className="flex flex-col items-center w-full space-y-4">
|
<div className="flex flex-col items-center w-full space-y-4">
|
||||||
<div className="purcarte-blur box-border border border-(--accent-a4) rounded-lg p-2">
|
<div className="purcarte-blur theme-card-style p-2">
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
variant={chartType === "load" ? "secondary" : "ghost"}
|
variant={chartType === "load" ? "secondary" : "ghost"}
|
||||||
@@ -149,7 +149,7 @@ const InstancePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`purcarte-blur box-border border border-(--accent-a4) justify-center rounded-lg p-2 ${
|
className={`purcarte-blur theme-card-style justify-center p-2 ${
|
||||||
isMobile ? "w-full" : ""
|
isMobile ? "w-full" : ""
|
||||||
}`}>
|
}`}>
|
||||||
{chartType === "load" ? (
|
{chartType === "load" ? (
|
||||||
|
Reference in New Issue
Block a user