mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-18 19:39:22 +08:00
feat: 初步适配主题颜色效果
This commit is contained in:
10
README.md
10
README.md
@@ -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 |
|
||||
|
||||
#### 标题栏设置
|
||||
|
||||
@@ -76,8 +80,6 @@
|
||||
| 启用标题栏标题 | `enableTitle` | `switch` | `true` | 启用后默认在顶栏左侧显示标题 |
|
||||
| 标题栏标题文本 | `titleText` | `string` | | 标题栏左侧显示的文本(留空则使用站点标题) |
|
||||
| 启用搜索按钮 | `enableSearchButton` | `switch` | `true` | 启用后默认在标题栏右侧显示搜索按钮 |
|
||||
| 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格(优先使用 localStorage) |
|
||||
| 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题(优先使用 localStorage) |
|
||||
| 启用管理按钮 | `enableAdminButton` | `switch` | `true` | 启用后默认在标题栏右侧显示管理按钮 |
|
||||
|
||||
#### 内容设置
|
||||
|
@@ -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": "启用管理按钮",
|
||||
|
@@ -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"
|
||||
|
@@ -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" />
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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: {
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -121,7 +121,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@apply border-(--accent-a4) outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background;
|
||||
|
47
src/main.tsx
47
src/main.tsx
@@ -19,36 +19,42 @@ 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">(() => {
|
||||
if (enableLocalStorage) {
|
||||
const savedMode = localStorage.getItem("nodeViewMode");
|
||||
return savedMode === "grid" || savedMode === "table"
|
||||
? savedMode
|
||||
: defaultView || "grid";
|
||||
const cleanedMode = savedMode ? savedMode.replace(/^"|"$/g, "") : null;
|
||||
if (cleanedMode === "grid" || cleanedMode === "table") {
|
||||
return cleanedMode;
|
||||
}
|
||||
}
|
||||
return defaultView || "grid";
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (enableLocalStorage) {
|
||||
localStorage.setItem("nodeViewMode", viewMode);
|
||||
}, [viewMode]);
|
||||
}
|
||||
}, [enableLocalStorage, viewMode]);
|
||||
|
||||
return (
|
||||
<ConfigProvider publicSettings={publicSettings}>
|
||||
<Theme
|
||||
appearance="inherit"
|
||||
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}
|
||||
theme={theme}
|
||||
toggleTheme={toggleTheme}
|
||||
appearance={appearance}
|
||||
setAppearance={setAppearance}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
/>
|
||||
@@ -56,9 +62,7 @@ const App = () => {
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<HomePage viewMode={viewMode} searchTerm={searchTerm} />
|
||||
}
|
||||
element={<HomePage viewMode={viewMode} searchTerm={searchTerm} />}
|
||||
/>
|
||||
<Route path="/instance/:uuid" element={<InstancePage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
@@ -67,6 +71,19 @@ const App = () => {
|
||||
<Footer />
|
||||
</div>
|
||||
</Theme>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const { publicSettings, loading } = useNodeData();
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider publicSettings={publicSettings}>
|
||||
<AppContent />
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
@@ -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={
|
||||
|
@@ -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);
|
||||
|
@@ -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" ? (
|
||||
|
Reference in New Issue
Block a user