feat: 添加状态卡片显示控制功能

This commit is contained in:
Montia37
2025-09-10 00:12:03 +08:00
parent 3251d69b92
commit 0e2bafcfce
6 changed files with 125 additions and 56 deletions

View File

@@ -70,10 +70,11 @@
| 磨砂玻璃背景色 | `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) |
| 启用标签透明背景 | `enableTransparentTags` | `switch` | `true` | 启用后标签将使用较为透明的背景色,当背景情况复杂导致标签难以辨识时建议关闭 | | 启用标签透明背景 | `enableTransparentTags` | `switch` | `true` | 启用后标签将使用较为透明的背景色,当背景情况复杂导致标签难以辨识时建议关闭 |
| 标签默认颜色列表 | `tagDefaultColorList` | `string` | `ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red` | 标签默认颜色列表展示的标签将按顺序调用该颜色池逗号分隔可用的颜色列表请参考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 ,改完没有生效则说明填写有误) |
| 默认主题颜色 | `selectThemeColor` | `select` | `gray` | 设置默认主题颜色颜色对照请参考https://www.radix-ui.com/themes/docs/theme/color |
| 启用 localStorage 配置 | `enableLocalStorage` | `switch` | `true` | 启用后将优先使用用户浏览器本地配置的视图和外观设置。关闭后将强制使用下方的主题配置,本地可调整但刷新即恢复 | | 启用 localStorage 配置 | `enableLocalStorage` | `switch` | `true` | 启用后将优先使用用户浏览器本地配置的视图和外观设置。关闭后将强制使用下方的主题配置,本地可调整但刷新即恢复 |
| 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格 | | 默认展示视图 | `selectedDefaultView` | `select` | `grid` | 设置默认展示视图为网格或表格 |
| 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题 | | 默认外观 | `selectedDefaultAppearance` | `select` | `system` | 设置默认外观为浅色、深色或系统主题 |
| 默认主题颜色 | `selectThemeColor` | `select` | `gray` | 设置默认主题颜色颜色对照请参考https://www.radix-ui.com/themes/docs/theme/color | | 状态卡片显示控制 | `statusCardsVisibility` | `string` | `currentTime:true,currentOnline:true,regionOverview:true,trafficOverview:true,networkSpeed:true` | 控制状态卡片的显示与隐藏,格式为 卡片名称:显示状态true/false多个卡片使用逗号分隔支持的卡片名称包括 currentTime当前时间, currentOnline当前在线, regionOverview点亮地区, trafficOverview流量概览, networkSpeed网络速率 |
#### 标题栏设置 #### 标题栏设置

View File

@@ -69,6 +69,14 @@
"default": "ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red", "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": "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"
},
{ {
"key": "enableLocalStorage", "key": "enableLocalStorage",
"name": "启用 localStorage 配置", "name": "启用 localStorage 配置",
@@ -93,12 +101,11 @@
"help": "设置默认外观为浅色、深色或系统主题" "help": "设置默认外观为浅色、深色或系统主题"
}, },
{ {
"key": "selectThemeColor", "key": "statusCardsVisibility",
"name": "默认主题颜色", "name": "状态卡片显示控制",
"type": "select", "type": "string",
"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": "currentTime:true,currentOnline:true,regionOverview:true,trafficOverview:true,networkSpeed:true",
"default": "gray", "help": "控制状态卡片的显示与隐藏,格式为 卡片名称:显示状态true/false多个卡片使用逗号分隔支持的卡片名称包括 currentTime当前时间, currentOnline当前在线, regionOverview点亮地区, trafficOverview流量概览, networkSpeed网络速率"
"help": "设置默认主题颜色颜色对照请参考https://www.radix-ui.com/themes/docs/theme/color"
}, },
{ {
"name": "标题栏设置", "name": "标题栏设置",

View File

@@ -15,11 +15,11 @@ import { useIsMobile } from "@/hooks/useMobile";
interface StatsBarProps { interface StatsBarProps {
displayOptions: { displayOptions: {
time: boolean; currentTime: boolean;
online: boolean; currentOnline: boolean;
regions: boolean; regionOverview: boolean;
traffic: boolean; trafficOverview: boolean;
speed: boolean; networkSpeed: boolean;
}; };
setDisplayOptions: (options: any) => void; setDisplayOptions: (options: any) => void;
stats: { stats: {
@@ -58,10 +58,10 @@ export const StatsBar = ({
// 渲染统计项 // 渲染统计项
const renderStatItem = (key: string) => { const renderStatItem = (key: string) => {
switch (key) { switch (key) {
case "time": case "currentTime":
return ( return (
displayOptions.time && ( displayOptions.currentTime && (
<div className="w-full py-1" key="time"> <div className="w-full py-1" key="currentTime">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-secondary-foreground text-sm"> <label className="text-secondary-foreground text-sm">
@@ -73,10 +73,10 @@ export const StatsBar = ({
</div> </div>
) )
); );
case "online": case "currentOnline":
return ( return (
displayOptions.online && ( displayOptions.currentOnline && (
<div className="w-full py-1" key="online"> <div className="w-full py-1" key="currentOnline">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-secondary-foreground text-sm"> <label className="text-secondary-foreground text-sm">
线 线
@@ -90,10 +90,10 @@ export const StatsBar = ({
</div> </div>
) )
); );
case "regions": case "regionOverview":
return ( return (
displayOptions.regions && ( displayOptions.regionOverview && (
<div className="w-full py-1" key="regions"> <div className="w-full py-1" key="regionOverview">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-secondary-foreground text-sm"> <label className="text-secondary-foreground text-sm">
@@ -105,10 +105,10 @@ export const StatsBar = ({
</div> </div>
) )
); );
case "traffic": case "trafficOverview":
return ( return (
displayOptions.traffic && ( displayOptions.trafficOverview && (
<div className="w-full py-1" key="traffic"> <div className="w-full py-1" key="trafficOverview">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-secondary-foreground text-sm"> <label className="text-secondary-foreground text-sm">
@@ -127,10 +127,10 @@ export const StatsBar = ({
</div> </div>
) )
); );
case "speed": case "networkSpeed":
return ( return (
displayOptions.speed && ( displayOptions.networkSpeed && (
<div className="w-full py-1" key="speed"> <div className="w-full py-1" key="networkSpeed">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-secondary-foreground text-sm"> <label className="text-secondary-foreground text-sm">
@@ -170,45 +170,60 @@ export const StatsBar = ({
<DropdownMenuItem className="flex items-center justify-between cursor-pointer"> <DropdownMenuItem className="flex items-center justify-between cursor-pointer">
<span></span> <span></span>
<Switch <Switch
checked={displayOptions.time} checked={displayOptions.currentTime}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDisplayOptions({ ...displayOptions, time: checked }) setDisplayOptions({
...displayOptions,
currentTime: checked,
})
} }
/> />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="flex items-center justify-between cursor-pointer"> <DropdownMenuItem className="flex items-center justify-between cursor-pointer">
<span>线</span> <span>线</span>
<Switch <Switch
checked={displayOptions.online} checked={displayOptions.currentOnline}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDisplayOptions({ ...displayOptions, online: checked }) setDisplayOptions({
...displayOptions,
currentOnline: checked,
})
} }
/> />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="flex items-center justify-between cursor-pointer"> <DropdownMenuItem className="flex items-center justify-between cursor-pointer">
<span></span> <span></span>
<Switch <Switch
checked={displayOptions.regions} checked={displayOptions.regionOverview}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDisplayOptions({ ...displayOptions, regions: checked }) setDisplayOptions({
...displayOptions,
regionOverview: checked,
})
} }
/> />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="flex items-center justify-between cursor-pointer"> <DropdownMenuItem className="flex items-center justify-between cursor-pointer">
<span></span> <span></span>
<Switch <Switch
checked={displayOptions.traffic} checked={displayOptions.trafficOverview}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDisplayOptions({ ...displayOptions, traffic: checked }) setDisplayOptions({
...displayOptions,
trafficOverview: checked,
})
} }
/> />
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="flex items-center justify-between cursor-pointer"> <DropdownMenuItem className="flex items-center justify-between cursor-pointer">
<span></span> <span></span>
<Switch <Switch
checked={displayOptions.speed} checked={displayOptions.networkSpeed}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setDisplayOptions({ ...displayOptions, speed: checked }) setDisplayOptions({
...displayOptions,
networkSpeed: checked,
})
} }
/> />
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -7,10 +7,11 @@ export interface ConfigOptions {
blurBackgroundColor?: string; // 磨砂玻璃背景颜色 blurBackgroundColor?: string; // 磨砂玻璃背景颜色
enableTransparentTags?: boolean; // 是否启用标签透明背景 enableTransparentTags?: boolean; // 是否启用标签透明背景
tagDefaultColorList?: string; // 标签默认颜色列表 tagDefaultColorList?: string; // 标签默认颜色列表
selectThemeColor?: string; // 默认主题颜色
enableLocalStorage?: boolean; // 是否启用本地存储 enableLocalStorage?: boolean; // 是否启用本地存储
selectedDefaultView?: "grid" | "table"; // 默认视图模式 selectedDefaultView?: "grid" | "table"; // 默认视图模式
selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式 selectedDefaultAppearance?: "light" | "dark" | "system"; // 默认外观模式
selectThemeColor?: string; // 默认主题颜色 statusCardsVisibility?: string; // 状态卡片显示控制
enableLogo?: boolean; // 是否启用Logo enableLogo?: boolean; // 是否启用Logo
logoUrl?: string; // Logo图片URL logoUrl?: string; // Logo图片URL
enableTitle?: boolean; // 是否启用标题 enableTitle?: boolean; // 是否启用标题
@@ -38,10 +39,12 @@ export const DEFAULT_CONFIG: ConfigOptions = {
enableTransparentTags: true, enableTransparentTags: true,
tagDefaultColorList: tagDefaultColorList:
"ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red", "ruby,gray,gold,bronze,brown,yellow,amber,orange,tomato,red",
selectThemeColor: "gray",
enableLocalStorage: true, enableLocalStorage: true,
selectedDefaultView: "grid", selectedDefaultView: "grid",
selectedDefaultAppearance: "system", selectedDefaultAppearance: "system",
selectThemeColor: "gray", statusCardsVisibility:
"currentTime:true,currentOnline:true,regionOverview:true,trafficOverview:true,networkSpeed:true",
enableLogo: false, enableLogo: false,
logoUrl: "/assets/logo.png", logoUrl: "/assets/logo.png",
enableTitle: true, enableTitle: true,

View File

@@ -44,6 +44,16 @@ export interface ThemeContextType {
setColor: (color: Colors) => void; setColor: (color: Colors) => void;
viewMode: "grid" | "table"; viewMode: "grid" | "table";
setViewMode: (mode: "grid" | "table") => void; setViewMode: (mode: "grid" | "table") => void;
statusCardsVisibility: {
currentTime: true;
currentOnline: true;
regionOverview: true;
trafficOverview: true;
networkSpeed: true;
};
setStatusCardsVisibility: (
visibility: Partial<ThemeContextType["statusCardsVisibility"]>
) => void;
} }
export const ThemeContext = createContext<ThemeContextType>({ export const ThemeContext = createContext<ThemeContextType>({
@@ -54,6 +64,14 @@ export const ThemeContext = createContext<ThemeContextType>({
setColor: () => {}, setColor: () => {},
viewMode: DEFAULT_CONFIG.selectedDefaultView as "grid" | "table", viewMode: DEFAULT_CONFIG.selectedDefaultView as "grid" | "table",
setViewMode: () => {}, setViewMode: () => {},
statusCardsVisibility: {
currentTime: true,
currentOnline: true,
regionOverview: true,
trafficOverview: true,
networkSpeed: true,
},
setStatusCardsVisibility: () => {},
}); });
/** /**
@@ -107,12 +125,17 @@ const useStoredState = <T>(
const [state, setState] = useState<T>(() => { const [state, setState] = useState<T>(() => {
if (enableLocalStorage) { if (enableLocalStorage) {
const storedValue = localStorage.getItem(key); try {
if (storedValue) { const storedValue = localStorage.getItem(key);
const cleanedValue = storedValue.replace(/^"|"$/g, ""); if (storedValue) {
if (!validator || validator(cleanedValue)) { const parsedValue = JSON.parse(storedValue);
return cleanedValue as T; if (!validator || validator(parsedValue)) {
return parsedValue as T;
}
} }
} catch (error) {
console.error("Error parsing stored state:", error);
// Fallback to default value if parsing fails
} }
} }
return defaultValue; return defaultValue;
@@ -120,7 +143,11 @@ const useStoredState = <T>(
useEffect(() => { useEffect(() => {
if (enableLocalStorage) { if (enableLocalStorage) {
localStorage.setItem(key, String(state)); try {
localStorage.setItem(key, JSON.stringify(state));
} catch (error) {
console.error("Error setting stored state:", error);
}
} }
}, [key, state, enableLocalStorage]); }, [key, state, enableLocalStorage]);
@@ -133,6 +160,9 @@ export const useThemeManager = () => {
) as Appearance; ) as Appearance;
const defaultColor = useConfigItem("selectThemeColor") as Colors; const defaultColor = useConfigItem("selectThemeColor") as Colors;
const defaultView = useConfigItem("selectedDefaultView") as "grid" | "table"; const defaultView = useConfigItem("selectedDefaultView") as "grid" | "table";
const defaultStatusCardsVisibility = useConfigItem(
"statusCardsVisibility"
) as string;
const [appearance, setAppearance] = useStoredState<Appearance>( const [appearance, setAppearance] = useStoredState<Appearance>(
"appearance", "appearance",
@@ -147,6 +177,24 @@ export const useThemeManager = () => {
defaultView defaultView
); );
const [statusCardsVisibility, setStatusCardsVisibility] = useStoredState(
"statusCardsVisibility",
(() => {
const visibility: { [key: string]: boolean } = {};
defaultStatusCardsVisibility.split(",").forEach((item) => {
const [key, value] = item.split(":");
visibility[key] = value === "true";
});
return visibility as ThemeContextType["statusCardsVisibility"];
})()
);
const handleSetStatusCardsVisibility = (
newVisibility: Partial<ThemeContextType["statusCardsVisibility"]>
) => {
setStatusCardsVisibility((prev) => ({ ...prev, ...newVisibility }));
};
useEffect(() => { useEffect(() => {
setColor(defaultColor); setColor(defaultColor);
}, [defaultColor, setColor]); }, [defaultColor, setColor]);
@@ -161,6 +209,8 @@ export const useThemeManager = () => {
setColor, setColor,
viewMode, viewMode,
setViewMode, setViewMode,
statusCardsVisibility,
setStatusCardsVisibility: handleSetStatusCardsVisibility,
}; };
}; };
export const useTheme = () => { export const useTheme = () => {

View File

@@ -29,7 +29,8 @@ const homeStateCache = {
}; };
const HomePage: React.FC<HomePageProps> = ({ searchTerm, setSearchTerm }) => { const HomePage: React.FC<HomePageProps> = ({ searchTerm, setSearchTerm }) => {
const { viewMode } = useTheme(); const { viewMode, statusCardsVisibility, setStatusCardsVisibility } =
useTheme();
const { nodes: staticNodes, loading, getGroups } = useNodeData(); const { nodes: staticNodes, loading, getGroups } = useNodeData();
const { liveData } = useLiveData(); const { liveData } = useLiveData();
const [selectedGroup, setSelectedGroup] = useState( const [selectedGroup, setSelectedGroup] = useState(
@@ -42,14 +43,6 @@ const HomePage: React.FC<HomePageProps> = ({ searchTerm, setSearchTerm }) => {
enableListItemProgressBar, enableListItemProgressBar,
selectTrafficProgressStyle, selectTrafficProgressStyle,
} = useAppConfig(); } = useAppConfig();
const [displayOptions, setDisplayOptions] = useState({
time: true,
online: true,
regions: true,
traffic: true,
speed: true,
});
const combinedNodes = useMemo<NodeWithStatus[]>(() => { const combinedNodes = useMemo<NodeWithStatus[]>(() => {
if (!staticNodes) return []; if (!staticNodes) return [];
return staticNodes.map((node) => { return staticNodes.map((node) => {
@@ -134,8 +127,8 @@ const HomePage: React.FC<HomePageProps> = ({ searchTerm, setSearchTerm }) => {
className="w-[90%] max-w-screen-2xl mx-auto flex-1 flex flex-col pb-10 overflow-y-auto"> className="w-[90%] max-w-screen-2xl mx-auto flex-1 flex flex-col pb-10 overflow-y-auto">
{enableStatsBar && ( {enableStatsBar && (
<StatsBar <StatsBar
displayOptions={displayOptions} displayOptions={statusCardsVisibility}
setDisplayOptions={setDisplayOptions} setDisplayOptions={setStatusCardsVisibility}
stats={stats} stats={stats}
loading={loading} loading={loading}
/> />