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:
12
README.md
12
README.md
@@ -64,8 +64,12 @@
|
|||||||
| :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
| 背景图片链接 | `backgroundImage` | `string` | `/assets/Moonlit-Scenery.webp` | 目前仅支持单张背景图片(eg: https://test.com/1.png) |
|
| 背景图片链接 | `backgroundImage` | `string` | `/assets/Moonlit-Scenery.webp` | 目前仅支持单张背景图片(eg: https://test.com/1.png) |
|
||||||
| 磨砂玻璃模糊值 | `blurValue` | `number` | `10` | 调整模糊值大小,数值越大模糊效果越明显,建议值为 5-20,为 0 则表示不启用模糊效果 |
|
| 磨砂玻璃模糊值 | `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)`) |
|
| 磨砂玻璃背景色 | `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),改完没有生效则说明填写有误) |
|
| 标签默认颜色列表 | `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 | `enableLogo` | `switch` | `false` | 启用后默认在标题栏左侧显示 Logo |
|
||||||
| Logo 图片链接 | `logoUrl` | `string` | `/assets/logo.png` | Logo 图片链接(eg: https://test.com/logo.png) |
|
| Logo 图片链接 | `logoUrl` | `string` | `/assets/logo.png` | Logo 图片链接(eg: https://test.com/logo.png) |
|
||||||
| 启用标题栏标题 | `enableTitle` | `switch` | `true` | 启用后默认在顶栏左侧显示标题 |
|
| 启用标题栏标题 | `enableTitle` | `switch` | `true` | 启用后默认在顶栏左侧显示标题 |
|
||||||
| 标题栏标题文本 | `titleText` | `string` | | 标题栏左侧显示的文本(留空则使用站点标题) |
|
| 标题栏标题文本 | `titleText` | `string` | | 标题栏左侧显示的文本(留空则使用站点标题) |
|
||||||
| 启用搜索按钮 | `enableSearchButton` | `switch` | `true` | 启用后默认在标题栏右侧显示搜索按钮 |
|
| 启用搜索按钮 | `enableSearchButton` | `switch` | `true` | 启用后默认在标题栏右侧显示搜索按钮 |
|
||||||
| 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格(优先使用 localStorage) |
|
|
||||||
| 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题(优先使用 localStorage) |
|
|
||||||
| 启用管理按钮 | `enableAdminButton` | `switch` | `true` | 启用后默认在标题栏右侧显示管理按钮 |
|
| 启用管理按钮 | `enableAdminButton` | `switch` | `true` | 启用后默认在标题栏右侧显示管理按钮 |
|
||||||
|
|
||||||
#### 内容设置
|
#### 内容设置
|
||||||
|
@@ -38,8 +38,39 @@
|
|||||||
"key": "tagDefaultColorList",
|
"key": "tagDefaultColorList",
|
||||||
"name": "标签默认颜色列表",
|
"name": "标签默认颜色列表",
|
||||||
"type": "string",
|
"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",
|
"default": "ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red",
|
||||||
"help": "标签默认颜色列表,用于修改默认解析颜色顺序以及使用的颜色池,逗号分隔(可用的颜色列表请参考:https://www.radix-ui.com/themes/docs/theme/color,改完没有生效则说明填写有误)"
|
"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": "标题栏设置",
|
"name": "标题栏设置",
|
||||||
@@ -81,22 +112,6 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"help": "启用后默认在标题栏右侧显示搜索按钮"
|
"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",
|
"key": "enableAdminButton",
|
||||||
"name": "启用管理按钮",
|
"name": "启用管理按钮",
|
||||||
|
@@ -2,8 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className="fixed inset-shadow-sm bottom-0 left-0 right-0 p-2 text-center purcarte-blur z-50">
|
<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 whitespace-pre">
|
<p className="flex justify-center text-sm text-second-foreground text-shadow-lg text-shadow-(color:--accent-a4) whitespace-pre">
|
||||||
Powered by{" "}
|
Powered by{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/komari-monitor/komari"
|
href="https://github.com/komari-monitor/komari"
|
||||||
|
@@ -13,6 +13,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useIsMobile } from "@/hooks/useMobile";
|
import { useIsMobile } from "@/hooks/useMobile";
|
||||||
import { useConfigItem } from "@/config";
|
import { useConfigItem } from "@/config";
|
||||||
|
import type { Appearance } from "@/hooks/useTheme";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -23,8 +24,8 @@ import {
|
|||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
viewMode: "grid" | "table";
|
viewMode: "grid" | "table";
|
||||||
setViewMode: (mode: "grid" | "table") => void;
|
setViewMode: (mode: "grid" | "table") => void;
|
||||||
theme: string;
|
appearance: Appearance;
|
||||||
toggleTheme: () => void;
|
setAppearance: (appearance: Appearance) => void;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
setSearchTerm: (term: string) => void;
|
setSearchTerm: (term: string) => void;
|
||||||
}
|
}
|
||||||
@@ -32,8 +33,8 @@ interface HeaderProps {
|
|||||||
export const Header = ({
|
export const Header = ({
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
theme,
|
appearance,
|
||||||
toggleTheme,
|
setAppearance,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
}: HeaderProps) => {
|
}: HeaderProps) => {
|
||||||
@@ -54,10 +55,15 @@ export const Header = ({
|
|||||||
}
|
}
|
||||||
}, [sitename]);
|
}, [sitename]);
|
||||||
|
|
||||||
|
const toggleAppearance = () => {
|
||||||
|
const newAppearance = appearance === "light" ? "dark" : "light";
|
||||||
|
setAppearance(newAppearance);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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="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">
|
<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" />
|
||||||
@@ -79,13 +85,13 @@ export const Header = ({
|
|||||||
className="relative group">
|
className="relative group">
|
||||||
<Search className="size-5 text-primary" />
|
<Search className="size-5 text-primary" />
|
||||||
{searchTerm && (
|
{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>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
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">
|
<div className="p-2">
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -107,12 +113,12 @@ export const Header = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="relative group">
|
className="relative group">
|
||||||
<Menu className="size-5 text-primary transition-transform duration-300 group-data-[state=open]:rotate-180" />
|
<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>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="purcarte-blur border-border rounded-xl">
|
className="purcarte-blur border-(--accent-a4) rounded-xl">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewMode(viewMode === "grid" ? "table" : "grid")
|
setViewMode(viewMode === "grid" ? "table" : "grid")
|
||||||
@@ -126,14 +132,14 @@ export const Header = ({
|
|||||||
{viewMode === "grid" ? "表格视图" : "网格视图"}
|
{viewMode === "grid" ? "表格视图" : "网格视图"}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={toggleTheme}>
|
<DropdownMenuItem onClick={toggleAppearance}>
|
||||||
{theme === "dark" ? (
|
{appearance === "dark" ? (
|
||||||
<Sun className="size-4 mr-2 text-primary" />
|
<Sun className="size-4 mr-2 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Moon className="size-4 mr-2 text-primary" />
|
<Moon className="size-4 mr-2 text-primary" />
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{theme === "dark" ? "浅色模式" : "深色模式"}
|
{appearance === "dark" ? "浅色模式" : "深色模式"}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{enableAdminButton && (
|
{enableAdminButton && (
|
||||||
@@ -177,7 +183,7 @@ export const Header = ({
|
|||||||
onClick={() => setIsSearchOpen(!isSearchOpen)}>
|
onClick={() => setIsSearchOpen(!isSearchOpen)}>
|
||||||
<Search className="size-5 text-primary" />
|
<Search className="size-5 text-primary" />
|
||||||
{searchTerm && (
|
{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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -193,8 +199,11 @@ export const Header = ({
|
|||||||
<Grid3X3 className="size-5 text-primary" />
|
<Grid3X3 className="size-5 text-primary" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
<Button
|
||||||
{theme === "dark" ? (
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleAppearance}>
|
||||||
|
{appearance === "dark" ? (
|
||||||
<Sun className="size-5 text-primary" />
|
<Sun className="size-5 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Moon className="size-5 text-primary" />
|
<Moon className="size-5 text-primary" />
|
||||||
@@ -226,14 +235,16 @@ export const Header = ({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-border rounded-xl">
|
className="animate-in slide-in-from-top-5 duration-300 purcarte-blur border-(--accent-a4) rounded-xl">
|
||||||
<DropdownMenuItem onClick={toggleTheme}>
|
<DropdownMenuItem onClick={toggleAppearance}>
|
||||||
{theme === "dark" ? (
|
{appearance === "dark" ? (
|
||||||
<Sun className="size-4 mr-2 text-primary" />
|
<Sun className="size-4 mr-2 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Moon className="size-4 mr-2 text-primary" />
|
<Moon className="size-4 mr-2 text-primary" />
|
||||||
)}
|
)}
|
||||||
<span>{theme === "dark" ? "浅色模式" : "深色模式"}</span>
|
<span>
|
||||||
|
{appearance === "dark" ? "浅色模式" : "深色模式"}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{enableAdminButton && (
|
{enableAdminButton && (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
@@ -251,8 +262,11 @@ export const Header = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
<Button
|
||||||
{theme === "dark" ? (
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleAppearance}>
|
||||||
|
{appearance === "dark" ? (
|
||||||
<Sun className="size-5 text-primary" />
|
<Sun className="size-5 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<Moon 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">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Tag tags={tagList} />
|
<Tag tags={tagList} />
|
||||||
</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 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-border/60 my-2"></div>
|
<div className="border-t border-(--accent-a4) 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-border/60 mx-2"></div>
|
<div className="border-l border-(--accent-a4) 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-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-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>
|
||||||
|
@@ -36,7 +36,7 @@ export const NodeListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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
|
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-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">
|
<div className="absolute top-2 right-2">
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
@@ -5,20 +5,19 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/utils";
|
import { cn } from "@/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
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:
|
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:
|
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:
|
secondary:
|
||||||
"bg-secondary/60 text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
"bg-(--accent-a5) text-secondary-foreground shadow-sm shadow-(color:--accent-a4) hover:bg-(--accent-a6)",
|
||||||
ghost:
|
ghost: "hover:bg-(--accent-a5) hover:bg-(--accent-a6)",
|
||||||
"hover:bg-accent/80 hover:text-accent-foreground dark:hover:bg-accent/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 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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -177,7 +177,7 @@ function ChartTooltipContent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
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-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
|
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-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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -11,7 +11,7 @@ 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-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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -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-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={{
|
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-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">
|
<p className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
{labelFormatter
|
{labelFormatter
|
||||||
? labelFormatter(label)
|
? labelFormatter(label)
|
||||||
|
@@ -16,30 +16,24 @@ export function ConfigProvider({
|
|||||||
publicSettings,
|
publicSettings,
|
||||||
children,
|
children,
|
||||||
}: ConfigProviderProps) {
|
}: ConfigProviderProps) {
|
||||||
const theme = useMemo(() => {
|
const config: ConfigOptions = useMemo(() => {
|
||||||
return (publicSettings?.theme_settings as ConfigOptions) || {};
|
const themeSettings =
|
||||||
}, [publicSettings?.theme_settings]);
|
(publicSettings?.theme_settings as ConfigOptions) || {};
|
||||||
|
const mergedConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...themeSettings,
|
||||||
|
titleText:
|
||||||
|
themeSettings.titleText ||
|
||||||
|
publicSettings?.sitename ||
|
||||||
|
DEFAULT_CONFIG.titleText,
|
||||||
|
};
|
||||||
|
|
||||||
// 使用 useMemo 缓存背景图片,避免每次渲染时重新计算
|
return mergedConfig;
|
||||||
const backgroundImage = useMemo(() => {
|
}, [publicSettings]);
|
||||||
return theme.backgroundImage || "";
|
|
||||||
}, [theme.backgroundImage]);
|
|
||||||
|
|
||||||
// 使用 useMemo 缓存模糊值,避免每次渲染时重新计算
|
|
||||||
const blurValue = useMemo(() => {
|
|
||||||
return theme.blurValue ?? DEFAULT_CONFIG.blurValue ?? 10;
|
|
||||||
}, [theme.blurValue]);
|
|
||||||
|
|
||||||
// 使用 useMemo 缓存模糊背景颜色,避免每次渲染时重新计算
|
|
||||||
const blurBackgroundColor = useMemo(() => {
|
|
||||||
return (
|
|
||||||
theme.blurBackgroundColor || DEFAULT_CONFIG.blurBackgroundColor || ""
|
|
||||||
);
|
|
||||||
}, [theme.blurBackgroundColor]);
|
|
||||||
|
|
||||||
// 合并的样式设置逻辑
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 设置背景图片
|
const { backgroundImage, blurValue, blurBackgroundColor } = config;
|
||||||
|
|
||||||
if (backgroundImage) {
|
if (backgroundImage) {
|
||||||
document.body.style.setProperty(
|
document.body.style.setProperty(
|
||||||
"--body-background-url",
|
"--body-background-url",
|
||||||
@@ -49,74 +43,24 @@ export function ConfigProvider({
|
|||||||
document.body.style.removeProperty("--body-background-url");
|
document.body.style.removeProperty("--body-background-url");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置模糊值
|
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--purcarte-blur",
|
"--purcarte-blur",
|
||||||
`${blurValue}px`
|
`${blurValue}px`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置模糊背景颜色(亮色/暗色模式)
|
|
||||||
if (blurBackgroundColor) {
|
if (blurBackgroundColor) {
|
||||||
// 解析颜色字符串,支持逗号分隔的亮色,暗色
|
|
||||||
const colors = blurBackgroundColor
|
const colors = blurBackgroundColor
|
||||||
.split("|")
|
.split("|")
|
||||||
.map((color) => color.trim());
|
.map((color) => color.trim());
|
||||||
if (colors.length >= 2) {
|
if (colors.length >= 2) {
|
||||||
// 第一个颜色用于亮色模式,第二个颜色用于暗色模式
|
|
||||||
document.documentElement.style.setProperty("--card-light", colors[0]);
|
document.documentElement.style.setProperty("--card-light", colors[0]);
|
||||||
document.documentElement.style.setProperty("--card-dark", colors[1]);
|
document.documentElement.style.setProperty("--card-dark", colors[1]);
|
||||||
} else if (colors.length === 1) {
|
} else if (colors.length === 1) {
|
||||||
// 只有一个颜色,同时用于亮色和暗色模式
|
|
||||||
document.documentElement.style.setProperty("--card-light", colors[0]);
|
document.documentElement.style.setProperty("--card-light", colors[0]);
|
||||||
document.documentElement.style.setProperty("--card-dark", colors[0]);
|
document.documentElement.style.setProperty("--card-dark", colors[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [backgroundImage, blurValue, blurBackgroundColor]);
|
}, [config]);
|
||||||
|
|
||||||
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,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
|
<ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
|
||||||
|
@@ -4,13 +4,15 @@ export interface ConfigOptions {
|
|||||||
blurValue?: number; // 磨砂玻璃模糊值
|
blurValue?: number; // 磨砂玻璃模糊值
|
||||||
blurBackgroundColor?: string; // 磨砂玻璃背景颜色
|
blurBackgroundColor?: string; // 磨砂玻璃背景颜色
|
||||||
tagDefaultColorList?: string; // 标签默认颜色列表
|
tagDefaultColorList?: string; // 标签默认颜色列表
|
||||||
|
enableLocalStorage?: boolean; // 是否启用本地存储
|
||||||
|
selectedDefaultView?: "grid" | "table"; // 默认视图模式
|
||||||
|
selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式
|
||||||
|
selectThemeColor?: string; // 默认主题颜色
|
||||||
enableLogo?: boolean; // 是否启用Logo
|
enableLogo?: boolean; // 是否启用Logo
|
||||||
logoUrl?: string; // Logo图片URL
|
logoUrl?: string; // Logo图片URL
|
||||||
enableTitle?: boolean; // 是否启用标题
|
enableTitle?: boolean; // 是否启用标题
|
||||||
titleText?: string; // 标题文本
|
titleText?: string; // 标题文本
|
||||||
enableSearchButton?: boolean; // 是否启用搜索按钮
|
enableSearchButton?: boolean; // 是否启用搜索按钮
|
||||||
selectedDefaultView?: "grid" | "table"; // 默认视图模式
|
|
||||||
selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式
|
|
||||||
enableAdminButton?: boolean; // 是否启用管理员按钮
|
enableAdminButton?: boolean; // 是否启用管理员按钮
|
||||||
enableStatsBar?: boolean; // 是否启用统计栏
|
enableStatsBar?: boolean; // 是否启用统计栏
|
||||||
enableGroupedBar?: boolean; // 是否启用分组栏
|
enableGroupedBar?: boolean; // 是否启用分组栏
|
||||||
@@ -28,14 +30,16 @@ export const DEFAULT_CONFIG: ConfigOptions = {
|
|||||||
blurValue: 10,
|
blurValue: 10,
|
||||||
blurBackgroundColor: "rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)",
|
blurBackgroundColor: "rgba(255, 255, 255, 0.5)|rgba(0, 0, 0, 0.5)",
|
||||||
tagDefaultColorList:
|
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,
|
enableLogo: false,
|
||||||
logoUrl: "/assets/logo.png",
|
logoUrl: "/assets/logo.png",
|
||||||
enableTitle: true,
|
enableTitle: true,
|
||||||
titleText: "Komari",
|
titleText: "Komari",
|
||||||
enableSearchButton: true,
|
enableSearchButton: true,
|
||||||
selectedDefaultView: "grid",
|
|
||||||
selectedDefaultAppearance: "system",
|
|
||||||
enableAdminButton: true,
|
enableAdminButton: true,
|
||||||
enableStatsBar: true,
|
enableStatsBar: true,
|
||||||
enableGroupedBar: true,
|
enableGroupedBar: true,
|
||||||
|
@@ -1,43 +1,121 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, createContext } from "react";
|
||||||
import { useConfigItem } from "@/config/hooks";
|
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 = () => {
|
export const useTheme = () => {
|
||||||
|
const enableLocalStorage = useConfigItem("enableLocalStorage");
|
||||||
const defaultAppearance = useConfigItem("selectedDefaultAppearance");
|
const defaultAppearance = useConfigItem("selectedDefaultAppearance");
|
||||||
|
const defaultColor = useConfigItem("selectThemeColor");
|
||||||
|
|
||||||
const [theme, setTheme] = useState<Theme>(() => {
|
console.log("Config in useTheme:", {
|
||||||
const storedTheme = localStorage.getItem("appearance");
|
enableLocalStorage,
|
||||||
if (
|
defaultAppearance,
|
||||||
storedTheme === "light" ||
|
defaultColor,
|
||||||
storedTheme === "dark" ||
|
});
|
||||||
storedTheme === "system"
|
|
||||||
) {
|
const [appearance, setAppearance] = useState<Appearance>(() => {
|
||||||
return storedTheme;
|
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(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement;
|
const root = window.document.documentElement;
|
||||||
root.classList.remove("light", "dark");
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
if (theme === "system") {
|
if (appearance === "system") {
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
.matches
|
.matches
|
||||||
? "dark"
|
? "dark"
|
||||||
: "light";
|
: "light";
|
||||||
root.classList.add(systemTheme);
|
root.classList.add(systemTheme);
|
||||||
} else {
|
} else {
|
||||||
root.classList.add(theme);
|
root.classList.add(appearance);
|
||||||
}
|
}
|
||||||
localStorage.setItem("appearance", theme);
|
if (enableLocalStorage) {
|
||||||
}, [theme]);
|
localStorage.setItem("appearance", appearance);
|
||||||
|
}
|
||||||
|
}, [appearance, enableLocalStorage]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
useEffect(() => {
|
||||||
const newTheme = theme === "light" ? "dark" : "light";
|
if (enableLocalStorage) {
|
||||||
setTheme(newTheme);
|
localStorage.setItem("color", color);
|
||||||
};
|
}
|
||||||
|
}, [color, enableLocalStorage]);
|
||||||
|
|
||||||
return { theme, toggleTheme };
|
return { appearance, setAppearance, color, setColor };
|
||||||
};
|
};
|
||||||
|
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-(--accent-a4) outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
|
93
src/main.tsx
93
src/main.tsx
@@ -19,54 +19,71 @@ const NotFoundPage = lazy(() => import("@/pages/NotFound"));
|
|||||||
|
|
||||||
import { useConfigItem } from "@/config";
|
import { useConfigItem } from "@/config";
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// 内部应用组件,在 ConfigProvider 内部使用配置
|
||||||
const App = () => {
|
export const AppContent = () => {
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { appearance, setAppearance, color } = useTheme();
|
||||||
const { publicSettings } = useNodeData();
|
|
||||||
const defaultView = useConfigItem("selectedDefaultView");
|
const defaultView = useConfigItem("selectedDefaultView");
|
||||||
|
const enableLocalStorage = useConfigItem("enableLocalStorage");
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "table">(() => {
|
const [viewMode, setViewMode] = useState<"grid" | "table">(() => {
|
||||||
const savedMode = localStorage.getItem("nodeViewMode");
|
if (enableLocalStorage) {
|
||||||
return savedMode === "grid" || savedMode === "table"
|
const savedMode = localStorage.getItem("nodeViewMode");
|
||||||
? savedMode
|
const cleanedMode = savedMode ? savedMode.replace(/^"|"$/g, "") : null;
|
||||||
: defaultView || "grid";
|
if (cleanedMode === "grid" || cleanedMode === "table") {
|
||||||
|
return cleanedMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultView || "grid";
|
||||||
});
|
});
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("nodeViewMode", viewMode);
|
if (enableLocalStorage) {
|
||||||
}, [viewMode]);
|
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 (
|
return (
|
||||||
<ConfigProvider publicSettings={publicSettings}>
|
<ConfigProvider publicSettings={publicSettings}>
|
||||||
<Theme
|
<AppContent />
|
||||||
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>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -133,7 +133,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-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>
|
<span>分组</span>
|
||||||
{groups.map((group: string) => (
|
{groups.map((group: string) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -155,7 +155,7 @@ const HomePage: React.FC<HomePageProps> = ({ viewMode, searchTerm }) => {
|
|||||||
className={
|
className={
|
||||||
viewMode === "grid"
|
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
|
<div
|
||||||
className={
|
className={
|
||||||
|
@@ -394,7 +394,8 @@ const PingChart = memo(({ node, hours }: PingChartProps) => {
|
|||||||
<Brush
|
<Brush
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
height={30}
|
height={30}
|
||||||
stroke="#8884d8"
|
stroke="var(--accent-track)"
|
||||||
|
fill="transparent"
|
||||||
alwaysShowText
|
alwaysShowText
|
||||||
tickFormatter={(time) => {
|
tickFormatter={(time) => {
|
||||||
const date = new Date(time);
|
const date = new Date(time);
|
||||||
|
@@ -108,11 +108,11 @@ 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 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">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<Button
|
<Button
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => navigate(-1)}>
|
onClick={() => navigate(-1)}>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
@@ -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-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">
|
<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-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" : ""
|
isMobile ? "w-full" : ""
|
||||||
}`}>
|
}`}>
|
||||||
{chartType === "load" ? (
|
{chartType === "load" ? (
|
||||||
|
Reference in New Issue
Block a user