feat: 初步适配主题颜色效果

This commit is contained in:
Montia37
2025-09-07 22:37:24 +08:00
parent b121bf13ac
commit eecdc179c3
23 changed files with 285 additions and 211 deletions

View File

@@ -64,8 +64,12 @@
| :--- | :--- | :--- | :--- | :--- |
| 背景图片链接 | `backgroundImage` | `string` | `/assets/Moonlit-Scenery.webp` | 目前仅支持单张背景图片eg: https://test.com/1.png |
| 磨砂玻璃模糊值 | `blurValue` | `number` | `10` | 调整模糊值大小,数值越大模糊效果越明显,建议值为 5-20为 0 则表示不启用模糊效果 |
| 磨砂玻璃背景色 | `blurBackgroundColor` | `string` | `rgba(255, 255, 255, 0.5) \| rgba(0, 0, 0, 0.5)` | 调整模糊背景色,推荐 rgba 颜色值,使用“\|”分隔亮色模式和暗色模式的颜色值eg: `rgba(255, 255, 255, 0.5)\|rgba(0, 0, 0, 0.5)` |
| 标签默认颜色列表 | `tagDefaultColorList` | `string` | `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` | 标签默认颜色列表,用于修改默认解析颜色顺序以及使用的颜色池,逗号分隔(可用的颜色列表请参考:[radix-ui color](https://www.radix-ui.com/themes/docs/theme/color),改完没有生效则说明填写有误) |
| 磨砂玻璃背景色 | `blurBackgroundColor` | `string` | `rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)` | 调整模糊背景色,推荐 rgba 颜色值,使用“|”分隔亮色模式和暗色模式的颜色值eg: rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5) |
| 标签默认颜色列表 | `tagDefaultColorList` | `string` | `ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red` | 标签默认颜色列表,展示的标签将按顺序调用该颜色池,逗号分隔(可用的颜色列表请参考:https://www.radix-ui.com/themes/docs/theme/color ,改完没有生效则说明填写有误) |
| 启用 localStorage 配置 | `enableLocalStorage` | `switch` | `true` | 启用后将优先使用用户浏览器本地配置的视图和外观设置。关闭后将强制使用下方的主题配置,本地可调整但刷新即恢复 |
| 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格 |
| 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题 |
| 默认主题颜色 | `selectThemeColor` | `select` | `gray` | 设置默认主题颜色颜色对照请参考https://www.radix-ui.com/themes/docs/theme/color |
#### 标题栏设置
@@ -74,10 +78,8 @@
| 启用标题栏左侧 Logo | `enableLogo` | `switch` | `false` | 启用后默认在标题栏左侧显示 Logo |
| Logo 图片链接 | `logoUrl` | `string` | `/assets/logo.png` | Logo 图片链接eg: https://test.com/logo.png |
| 启用标题栏标题 | `enableTitle` | `switch` | `true` | 启用后默认在顶栏左侧显示标题 |
| 标题栏标题文本 | `titleText` | `string` | | 标题栏左侧显示的文本(留空则使用站点标题) |
| 标题栏标题文本 | `titleText` | `string` | | 标题栏左侧显示的文本(留空则使用站点标题) |
| 启用搜索按钮 | `enableSearchButton` | `switch` | `true` | 启用后默认在标题栏右侧显示搜索按钮 |
| 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格(优先使用 localStorage |
| 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题(优先使用 localStorage |
| 启用管理按钮 | `enableAdminButton` | `switch` | `true` | 启用后默认在标题栏右侧显示管理按钮 |
#### 内容设置

View File

@@ -38,8 +38,39 @@
"key": "tagDefaultColorList",
"name": "标签默认颜色列表",
"type": "string",
"default": "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",
"help": "标签默认颜色列表,用于修改默认解析颜色顺序以及使用的颜色池逗号分隔可用的颜色列表请参考https://www.radix-ui.com/themes/docs/theme/color改完没有生效则说明填写有误"
"default": "ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red",
"help": "标签默认颜色列表,展示的标签将按顺序调用该颜色池逗号分隔可用的颜色列表请参考https://www.radix-ui.com/themes/docs/theme/color改完没有生效则说明填写有误"
},
{
"key": "enableLocalStorage",
"name": "启用 localStorage 配置",
"type": "switch",
"default": true,
"help": "启用后将优先使用用户浏览器本地配置的视图和外观设置。关闭后将强制使用下方的主题配置,本地可调整但刷新即恢复"
},
{
"key": "selectedDefaultView",
"name": "默认展示视图",
"type": "select",
"options": "grid,table",
"default": "grid",
"help": "设置默认展示视图为网格或表格"
},
{
"key": "selectedDefaultAppearance",
"name": "默认外观",
"type": "select",
"options": "system,light,dark",
"default": "system",
"help": "设置默认外观为浅色、深色或系统主题"
},
{
"key": "selectThemeColor",
"name": "默认主题颜色",
"type": "select",
"options": "gray,gold,bronze,brown,yellow,amber,orange,tomato,red,ruby,crimson,pink,plum,purple,violet,iris,indigo,blue,cyan,teal,jade,green,grass,lime,mint,sky",
"default": "gray",
"help": "设置默认主题颜色颜色对照请参考https://www.radix-ui.com/themes/docs/theme/color"
},
{
"name": "标题栏设置",
@@ -81,22 +112,6 @@
"default": true,
"help": "启用后默认在标题栏右侧显示搜索按钮"
},
{
"key": "selectedDefaultView",
"name": "默认展示视图",
"type": "select",
"options": "grid,table",
"default": "grid",
"help": "设置默认展示视图为网格或表格(优先使用 localStorage"
},
{
"key": "selectedDefaultAppearance",
"name": "默认外观",
"type": "select",
"options": "system,light,dark",
"default": "system",
"help": "设置默认外观为浅色、深色或系统主题(优先使用 localStorage"
},
{
"key": "enableAdminButton",
"name": "启用管理按钮",

View File

@@ -2,8 +2,8 @@ import React from "react";
const Footer: React.FC = () => {
return (
<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">
<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">
<p className="flex justify-center text-sm text-second-foreground text-shadow-lg text-shadow-(color:--accent-a4) whitespace-pre">
Powered by{" "}
<a
href="https://github.com/komari-monitor/komari"

View File

@@ -13,6 +13,7 @@ import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useIsMobile } from "@/hooks/useMobile";
import { useConfigItem } from "@/config";
import type { Appearance } from "@/hooks/useTheme";
import {
DropdownMenu,
DropdownMenuContent,
@@ -23,8 +24,8 @@ import {
interface HeaderProps {
viewMode: "grid" | "table";
setViewMode: (mode: "grid" | "table") => void;
theme: string;
toggleTheme: () => void;
appearance: Appearance;
setAppearance: (appearance: Appearance) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
}
@@ -32,8 +33,8 @@ interface HeaderProps {
export const Header = ({
viewMode,
setViewMode,
theme,
toggleTheme,
appearance,
setAppearance,
searchTerm,
setSearchTerm,
}: HeaderProps) => {
@@ -54,10 +55,15 @@ export const Header = ({
}
}, [sitename]);
const toggleAppearance = () => {
const newAppearance = appearance === "light" ? "dark" : "light";
setAppearance(newAppearance);
};
return (
<header className="purcarte-blur border-b border-border sticky top-0 flex items-center justify-center shadow-sm z-10">
<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">
<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">
<div className="flex items-center text-shadow-lg text-shadow-(color:--accent-a4) text-accent-foreground">
<a href="/" className="flex items-center gap-2 text-2xl font-bold">
{enableLogo && logoUrl && (
<img src={logoUrl} alt="logo" className="h-8" />
@@ -79,13 +85,13 @@ export const Header = ({
className="relative group">
<Search className="size-5 text-primary" />
{searchTerm && (
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-primary transform -translate-x-1/2"></span>
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-(--accent-indicator) transform -translate-x-1/2"></span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="purcarte-blur border-border rounded-xl w-48">
className="purcarte-blur border-(--accent-a4) rounded-xl w-48">
<div className="p-2">
<Input
type="search"
@@ -107,12 +113,12 @@ export const Header = ({
size="icon"
className="relative group">
<Menu className="size-5 text-primary transition-transform duration-300 group-data-[state=open]:rotate-180" />
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-primary transform -translate-x-1/2 scale-0 transition-transform duration-300 group-data-[state=open]:scale-100"></span>
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-(--accent-indicator) transform -translate-x-1/2 scale-0 transition-transform duration-300 group-data-[state=open]:scale-100"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="purcarte-blur border-border rounded-xl">
className="purcarte-blur border-(--accent-a4) rounded-xl">
<DropdownMenuItem
onClick={() =>
setViewMode(viewMode === "grid" ? "table" : "grid")
@@ -126,14 +132,14 @@ export const Header = ({
{viewMode === "grid" ? "表格视图" : "网格视图"}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={toggleTheme}>
{theme === "dark" ? (
<DropdownMenuItem onClick={toggleAppearance}>
{appearance === "dark" ? (
<Sun className="size-4 mr-2 text-primary" />
) : (
<Moon className="size-4 mr-2 text-primary" />
)}
<span>
{theme === "dark" ? "浅色模式" : "深色模式"}
{appearance === "dark" ? "浅色模式" : "深色模式"}
</span>
</DropdownMenuItem>
{enableAdminButton && (
@@ -177,7 +183,7 @@ export const Header = ({
onClick={() => setIsSearchOpen(!isSearchOpen)}>
<Search className="size-5 text-primary" />
{searchTerm && (
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-primary transform -translate-x-1/2"></span>
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-(--accent-indicator) transform -translate-x-1/2"></span>
)}
</Button>
)}
@@ -193,8 +199,11 @@ export const Header = ({
<Grid3X3 className="size-5 text-primary" />
)}
</Button>
<Button variant="ghost" size="icon" onClick={toggleTheme}>
{theme === "dark" ? (
<Button
variant="ghost"
size="icon"
onClick={toggleAppearance}>
{appearance === "dark" ? (
<Sun className="size-5 text-primary" />
) : (
<Moon className="size-5 text-primary" />
@@ -226,14 +235,16 @@ export const Header = ({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-border rounded-xl">
<DropdownMenuItem onClick={toggleTheme}>
{theme === "dark" ? (
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-(--accent-a4) rounded-xl">
<DropdownMenuItem onClick={toggleAppearance}>
{appearance === "dark" ? (
<Sun className="size-4 mr-2 text-primary" />
) : (
<Moon className="size-4 mr-2 text-primary" />
)}
<span>{theme === "dark" ? "浅色模式" : "深色模式"}</span>
<span>
{appearance === "dark" ? "浅色模式" : "深色模式"}
</span>
</DropdownMenuItem>
{enableAdminButton && (
<DropdownMenuItem asChild>
@@ -251,8 +262,11 @@ export const Header = ({
</DropdownMenu>
) : (
<>
<Button variant="ghost" size="icon" onClick={toggleTheme}>
{theme === "dark" ? (
<Button
variant="ghost"
size="icon"
onClick={toggleAppearance}>
{appearance === "dark" ? (
<Sun className="size-5 text-primary" />
) : (
<Moon className="size-5 text-primary" />

View File

@@ -58,7 +58,7 @@ export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
<div className="flex flex-wrap gap-1">
<Tag tags={tagList} />
</div>
<div className="border-t border-border/60 my-2"></div>
<div className="border-t border-(--accent-a4) my-2"></div>
<div className="flex items-center justify-around whitespace-nowrap">
<div className="flex items-center gap-1">
<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>
</div>
</div>
<div className="border-t border-border/60 my-2"></div>
<div className="border-t border-(--accent-a4) my-2"></div>
<div className="flex justify-between text-xs">
<span className="text-secondary-foreground"></span>
<div>
@@ -167,7 +167,7 @@ export const NodeCard = ({ node, enableSwap }: NodeCardProps) => {
{expired_at}
</span>
</div>
<div className="border-l border-border/60 mx-2"></div>
<div className="border-l border-(--accent-a4) mx-2"></div>
<div className="flex justify-end w-full">
<span className="text-secondary-foreground">
{isOnline && stats

View File

@@ -6,7 +6,7 @@ export const NodeListHeader = ({ enableSwap }: NodeListHeaderProps) => {
const gridCols = enableSwap ? "grid-cols-10" : "grid-cols-9";
return (
<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`}>
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`}>
<div className="col-span-2"></div>
<div className="col-span-1">CPU</div>
<div className="col-span-1"></div>

View File

@@ -36,7 +36,7 @@ export const NodeListItem = ({
return (
<div
className={`grid ${gridCols} text-center shadow-md gap-4 p-2 text-nowrap items-center rounded-lg ${
className={`grid ${gridCols} text-center shadow-sm shadow-(color:--accent-a4) gap-4 p-2 text-nowrap items-center rounded-lg ${
isOnline
? ""
: "striped-bg-red-translucent-diagonal ring-2 ring-red-500/50"

View File

@@ -156,7 +156,7 @@ export const StatsBar = ({
}
};
return (
<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="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="absolute top-2 right-2">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>

View File

@@ -5,20 +5,19 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary/60 text-primary-foreground shadow-xs hover:bg-primary/80",
"bg-(--accent-a5) text-primary-foreground shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6)",
destructive:
"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",
"bg-(--accent-a5) text-white shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6) focus-visible:ring-destructive/20",
outline:
"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",
"border border-(--accent-a6) bg-(--accent-a5) shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6) hover:text-accent-foreground",
secondary:
"bg-secondary/60 text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent/80 hover:text-accent-foreground dark:hover:bg-accent/50",
"bg-(--accent-a5) text-secondary-foreground shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6)",
ghost: "hover:bg-(--accent-a5) hover:bg-(--accent-a6)",
link: "text-primary underline-offset-4 hover:underline",
},
size: {

View File

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

View File

@@ -177,7 +177,7 @@ function ChartTooltipContent({
return (
<div
className={cn(
"border-border/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",
"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",
className
)}>
{!nestLabel ? tooltipLabel : null}

View File

@@ -45,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-(--accent-a4) bg-popover p-1 text-popover-foreground shadow-md",
className
)}
{...props}
@@ -64,7 +64,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
"z-50 min-w-[8rem] purcarte-blur overflow-hidden rounded-md border border-(--accent-a4) bg-popover p-1 text-popover-foreground shadow-md",
className
)}
{...props}

View File

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

View File

@@ -68,7 +68,7 @@ const Tips: React.FC<TipsProps & React.HTMLAttributes<HTMLDivElement>> = ({
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"
className="purcarte-blur border border-(--accent-a4) shadow-md rounded-md z-50 text-muted-foreground"
style={{
minWidth: isMobile ? "12rem" : "16rem",
maxWidth: isMobile ? "80vw" : "16rem",

View File

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

View File

@@ -16,30 +16,24 @@ export function ConfigProvider({
publicSettings,
children,
}: ConfigProviderProps) {
const theme = useMemo(() => {
return (publicSettings?.theme_settings as ConfigOptions) || {};
}, [publicSettings?.theme_settings]);
const config: ConfigOptions = useMemo(() => {
const themeSettings =
(publicSettings?.theme_settings as ConfigOptions) || {};
const mergedConfig = {
...DEFAULT_CONFIG,
...themeSettings,
titleText:
themeSettings.titleText ||
publicSettings?.sitename ||
DEFAULT_CONFIG.titleText,
};
// 使用 useMemo 缓存背景图片,避免每次渲染时重新计算
const backgroundImage = useMemo(() => {
return theme.backgroundImage || "";
}, [theme.backgroundImage]);
return mergedConfig;
}, [publicSettings]);
// 使用 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(() => {
// 设置背景图片
const { backgroundImage, blurValue, blurBackgroundColor } = config;
if (backgroundImage) {
document.body.style.setProperty(
"--body-background-url",
@@ -49,74 +43,24 @@ export function ConfigProvider({
document.body.style.removeProperty("--body-background-url");
}
// 设置模糊值
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(
() => ({
tagDefaultColorList:
theme.tagDefaultColorList || DEFAULT_CONFIG.tagDefaultColorList,
enableLogo: theme.enableLogo ?? DEFAULT_CONFIG.enableLogo,
logoUrl: theme.logoUrl || DEFAULT_CONFIG.logoUrl,
enableTitle: theme.enableTitle ?? DEFAULT_CONFIG.enableTitle,
titleText:
theme.titleText || publicSettings?.sitename || DEFAULT_CONFIG.titleText,
enableSearchButton:
theme.enableSearchButton ?? DEFAULT_CONFIG.enableSearchButton,
selectedDefaultView:
theme.selectedDefaultView || DEFAULT_CONFIG.selectedDefaultView,
selectedDefaultAppearance:
theme.selectedDefaultAppearance ||
DEFAULT_CONFIG.selectedDefaultAppearance,
enableAdminButton:
theme.enableAdminButton ?? DEFAULT_CONFIG.enableAdminButton,
enableStatsBar: theme.enableStatsBar ?? DEFAULT_CONFIG.enableStatsBar,
enableGroupedBar:
theme.enableGroupedBar ?? DEFAULT_CONFIG.enableGroupedBar,
enableInstanceDetail:
theme.enableInstanceDetail ?? DEFAULT_CONFIG.enableInstanceDetail,
enablePingChart: theme.enablePingChart ?? DEFAULT_CONFIG.enablePingChart,
enableConnectBreaks:
theme.enableConnectBreaks ?? DEFAULT_CONFIG.enableConnectBreaks,
pingChartMaxPoints:
theme.pingChartMaxPoints || DEFAULT_CONFIG.pingChartMaxPoints,
backgroundImage,
blurValue,
blurBackgroundColor,
enableSwap: theme.enableSwap ?? DEFAULT_CONFIG.enableSwap,
enableListItemProgressBar:
theme.enableListItemProgressBar ??
DEFAULT_CONFIG.enableListItemProgressBar,
}),
[
theme,
backgroundImage,
blurValue,
blurBackgroundColor,
publicSettings?.sitename,
]
);
}, [config]);
return (
<ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>

View File

@@ -4,13 +4,15 @@ export interface ConfigOptions {
blurValue?: number; // 磨砂玻璃模糊值
blurBackgroundColor?: string; // 磨砂玻璃背景颜色
tagDefaultColorList?: string; // 标签默认颜色列表
enableLocalStorage?: boolean; // 是否启用本地存储
selectedDefaultView?: "grid" | "table"; // 默认视图模式
selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式
selectThemeColor?: string; // 默认主题颜色
enableLogo?: boolean; // 是否启用Logo
logoUrl?: string; // Logo图片URL
enableTitle?: boolean; // 是否启用标题
titleText?: string; // 标题文本
enableSearchButton?: boolean; // 是否启用搜索按钮
selectedDefaultView?: "grid" | "table"; // 默认视图模式
selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式
enableAdminButton?: boolean; // 是否启用管理员按钮
enableStatsBar?: boolean; // 是否启用统计栏
enableGroupedBar?: boolean; // 是否启用分组栏
@@ -28,14 +30,16 @@ export const DEFAULT_CONFIG: ConfigOptions = {
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",
"ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red",
enableLocalStorage: true,
selectedDefaultView: "grid",
selectedDefaultAppearance: "system",
selectThemeColor: "gray",
enableLogo: false,
logoUrl: "/assets/logo.png",
enableTitle: true,
titleText: "Komari",
enableSearchButton: true,
selectedDefaultView: "grid",
selectedDefaultAppearance: "system",
enableAdminButton: true,
enableStatsBar: true,
enableGroupedBar: true,

View File

@@ -1,43 +1,121 @@
import { useState, useEffect } from "react";
import { useConfigItem } from "@/config/hooks";
import { useState, useEffect, createContext } from "react";
import { useConfigItem } from "@/config";
type Theme = "light" | "dark" | "system";
export const allowedColors = [
"gray",
"gold",
"bronze",
"brown",
"yellow",
"amber",
"orange",
"tomato",
"red",
"ruby",
"crimson",
"pink",
"plum",
"purple",
"violet",
"iris",
"indigo",
"blue",
"cyan",
"teal",
"jade",
"green",
"grass",
"lime",
"mint",
"sky",
] as const;
export type Colors = (typeof allowedColors)[number];
export const allowedAppearances = ["light", "dark", "system"] as const;
export type Appearance = (typeof allowedAppearances)[number];
export const THEME_DEFAULTS = {
appearance: "system" as Appearance,
color: "gray" as Colors,
} as const;
export interface ThemeContextType {
appearance: Appearance;
setAppearance: (appearance: Appearance) => void;
color: Colors;
setColor: (color: Colors) => void;
}
export const ThemeContext = createContext<ThemeContextType>({
appearance: THEME_DEFAULTS.appearance,
setAppearance: () => {},
color: THEME_DEFAULTS.color,
setColor: () => {},
});
export const useTheme = () => {
const enableLocalStorage = useConfigItem("enableLocalStorage");
const defaultAppearance = useConfigItem("selectedDefaultAppearance");
const defaultColor = useConfigItem("selectThemeColor");
const [theme, setTheme] = useState<Theme>(() => {
const storedTheme = localStorage.getItem("appearance");
if (
storedTheme === "light" ||
storedTheme === "dark" ||
storedTheme === "system"
) {
return storedTheme;
console.log("Config in useTheme:", {
enableLocalStorage,
defaultAppearance,
defaultColor,
});
const [appearance, setAppearance] = useState<Appearance>(() => {
if (enableLocalStorage) {
const storedAppearance = localStorage.getItem("appearance");
const cleanedAppearance = storedAppearance
? storedAppearance.replace(/^"|"$/g, "")
: null;
if (allowedAppearances.includes(cleanedAppearance as Appearance)) {
return cleanedAppearance as Appearance;
}
}
return (defaultAppearance as Theme) || "system";
return (defaultAppearance as Appearance) || THEME_DEFAULTS.appearance;
});
const [color, setColor] = useState<Colors>(() => {
if (enableLocalStorage) {
const storedColor = localStorage.getItem("color");
const cleanedColor = storedColor
? storedColor.replace(/^"|"$/g, "")
: null;
if (allowedColors.includes(cleanedColor as Colors)) {
return cleanedColor as Colors;
}
}
console.log("defaultColor in useState:", defaultColor);
return (defaultColor as Colors) || THEME_DEFAULTS.color;
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
if (appearance === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
root.classList.add(appearance);
}
localStorage.setItem("appearance", theme);
}, [theme]);
if (enableLocalStorage) {
localStorage.setItem("appearance", appearance);
}
}, [appearance, enableLocalStorage]);
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
};
useEffect(() => {
if (enableLocalStorage) {
localStorage.setItem("color", color);
}
}, [color, enableLocalStorage]);
return { theme, toggleTheme };
return { appearance, setAppearance, color, setColor };
};

View File

@@ -121,7 +121,7 @@
@layer base {
* {
@apply border-border outline-ring/50;
@apply border-(--accent-a4) outline-ring/50;
}
body {
@apply bg-background;

View File

@@ -19,54 +19,71 @@ const NotFoundPage = lazy(() => import("@/pages/NotFound"));
import { useConfigItem } from "@/config";
// eslint-disable-next-line react-refresh/only-export-components
const App = () => {
const { theme, toggleTheme } = useTheme();
const { publicSettings } = useNodeData();
// 内部应用组件,在 ConfigProvider 内部使用配置
export const AppContent = () => {
const { appearance, setAppearance, color } = useTheme();
const defaultView = useConfigItem("selectedDefaultView");
const enableLocalStorage = useConfigItem("enableLocalStorage");
const [viewMode, setViewMode] = useState<"grid" | "table">(() => {
const savedMode = localStorage.getItem("nodeViewMode");
return savedMode === "grid" || savedMode === "table"
? savedMode
: defaultView || "grid";
if (enableLocalStorage) {
const savedMode = localStorage.getItem("nodeViewMode");
const cleanedMode = savedMode ? savedMode.replace(/^"|"$/g, "") : null;
if (cleanedMode === "grid" || cleanedMode === "table") {
return cleanedMode;
}
}
return defaultView || "grid";
});
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
localStorage.setItem("nodeViewMode", viewMode);
}, [viewMode]);
if (enableLocalStorage) {
localStorage.setItem("nodeViewMode", viewMode);
}
}, [enableLocalStorage, viewMode]);
return (
<Theme
appearance={appearance === "system" ? "inherit" : appearance}
accentColor={color}
scaling="110%"
style={{ backgroundColor: "transparent" }}>
<div className="min-h-screen flex flex-col text-sm">
<Header
viewMode={viewMode}
setViewMode={setViewMode}
appearance={appearance}
setAppearance={setAppearance}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
<Suspense fallback={<Loading />}>
<Routes>
<Route
path="/"
element={<HomePage viewMode={viewMode} searchTerm={searchTerm} />}
/>
<Route path="/instance/:uuid" element={<InstancePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
<Footer />
</div>
</Theme>
);
};
const App = () => {
const { publicSettings, loading } = useNodeData();
if (loading) {
return <Loading />;
}
return (
<ConfigProvider publicSettings={publicSettings}>
<Theme
appearance="inherit"
scaling="110%"
style={{ backgroundColor: "transparent" }}>
<div className="min-h-screen flex flex-col text-sm">
<Header
viewMode={viewMode}
setViewMode={setViewMode}
theme={theme}
toggleTheme={toggleTheme}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
<Suspense fallback={<Loading />}>
<Routes>
<Route
path="/"
element={
<HomePage viewMode={viewMode} searchTerm={searchTerm} />
}
/>
<Route path="/instance/:uuid" element={<InstancePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
<Footer />
</div>
</Theme>
<AppContent />
</ConfigProvider>
);
};

View File

@@ -133,7 +133,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 border-border space-x-4 px-4 rounded-lg mb-4 purcarte-blur">
<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">
<span></span>
{groups.map((group: string) => (
<Button
@@ -155,7 +155,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
className={
viewMode === "grid"
? ""
: "space-y-2 overflow-auto box-border border border-border purcarte-blur rounded-lg p-2"
: "space-y-2 overflow-auto box-border border border-(--accent-a4) purcarte-blur rounded-lg p-2"
}>
<div
className={

View File

@@ -394,7 +394,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
<Brush
dataKey="time"
height={30}
stroke="#8884d8"
stroke="var(--accent-track)"
fill="transparent"
alwaysShowText
tickFormatter={(time) => {
const date = new Date(time);

View File

@@ -108,11 +108,11 @@ 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 purcarte-blur box-border border border-border rounded-lg p-4 mb-4 text-secondary-foreground">
<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 gap-2 min-w-0">
<Button
className="flex-shrink-0"
variant="ghost"
variant="outline"
size="icon"
onClick={() => navigate(-1)}>
<ArrowLeft />
@@ -130,7 +130,7 @@ const InstancePage = () => {
{enableInstanceDetail && <Instance node={node as NodeWithStatus} />}
<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="purcarte-blur box-border border border-(--accent-a4) rounded-lg p-2">
<div className="flex justify-center space-x-2">
<Button
variant={chartType === "load" ? "secondary" : "ghost"}
@@ -149,7 +149,7 @@ const InstancePage = () => {
</div>
</div>
<div
className={`purcarte-blur box-border border border-border justify-center rounded-lg p-2 ${
className={`purcarte-blur box-border border border-(--accent-a4) justify-center rounded-lg p-2 ${
isMobile ? "w-full" : ""
}`}>
{chartType === "load" ? (