优化前端代码
This commit is contained in:
1 parent
a8d1d54c6a
commit
a5ee886f2d
10 files changed
+54
-271
No files matched your search
+1
-52
@@ -3,64 +3,13 @@
|
||||
/* Tailwind v4 暗色模式配置 */
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* 全局样式 */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* 全局背景色 */
|
||||
body {
|
||||
background-color: white;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgb(241, 245, 249);
|
||||
border-radius: 5px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(203, 213, 225);
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgb(241, 245, 249);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(148, 163, 184);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: rgb(100, 116, 139);
|
||||
}
|
||||
|
||||
/* 暗色模式滚动条 */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: rgb(30, 41, 59);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgb(71, 85, 105);
|
||||
border-color: rgb(30, 41, 59);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(100, 116, 139);
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:active {
|
||||
background: rgb(148, 163, 184);
|
||||
background-color: #05050a;
|
||||
}
|
||||
|
||||
/* 标签过滤器滚动条样式 */
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {ReactNode} from 'react';
|
||||
import {cn} from '@/lib/utils';
|
||||
import CyberCard from "@portal/components/CyberCard.tsx";
|
||||
|
||||
interface CardProps {
|
||||
@@ -7,7 +6,7 @@ interface CardProps {
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
children: ReactNode;
|
||||
variant?: 'light' | 'dark';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
@@ -15,30 +14,20 @@ export const Card = ({
|
||||
description,
|
||||
action,
|
||||
children,
|
||||
variant = 'light',
|
||||
className,
|
||||
}: CardProps) => {
|
||||
const isDark = variant === 'dark';
|
||||
|
||||
return (
|
||||
<CyberCard className={'p-6'}>
|
||||
<CyberCard className={className || 'p-6'}>
|
||||
{(title || description || action) && (
|
||||
<div className={cn(
|
||||
"flex flex-col gap-3 border-b pb-4 sm:flex-row sm:items-start sm:justify-between",
|
||||
isDark ? "border-cyan-900/30" : "border-slate-200 dark:border-slate-700"
|
||||
)}>
|
||||
<div className="flex flex-col gap-3 border-b border-slate-200 dark:border-slate-700 pb-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
{title && (
|
||||
<h2 className={cn(
|
||||
"text-sm font-bold",
|
||||
isDark
|
||||
? "font-mono uppercase tracking-widest text-cyan-500"
|
||||
: "text-lg font-semibold text-slate-900 dark:text-white"
|
||||
)}>
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className={'text-xs text-gray-600 dark:text-cyan-500 mt-1 font-mono'}>
|
||||
<p className="text-xs text-gray-600 dark:text-cyan-500 mt-1 font-mono">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -2,43 +2,31 @@ import {TrendingUp} from 'lucide-react';
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
interface ChartPlaceholderProps {
|
||||
variant?: 'light' | 'dark';
|
||||
icon?: typeof TrendingUp;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
heightClass?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ChartPlaceholder = ({
|
||||
variant = 'light',
|
||||
icon: Icon = TrendingUp,
|
||||
title = '暂无数据',
|
||||
subtitle = '等待采集新数据后展示图表',
|
||||
heightClass = 'h-52',
|
||||
className,
|
||||
}: ChartPlaceholderProps) => {
|
||||
const isDark = variant === 'dark';
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex items-center justify-center rounded-lg border border-dashed text-sm",
|
||||
isDark
|
||||
? "border-cyan-500/30 bg-[#0a0b10]/50 text-cyan-500"
|
||||
: "border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400",
|
||||
heightClass
|
||||
"flex items-center justify-center rounded-lg border border-dashed border-slate-200 dark:border-slate-700 text-sm text-slate-500 dark:text-slate-400",
|
||||
heightClass,
|
||||
className
|
||||
)}>
|
||||
<div className="text-center">
|
||||
<Icon className={cn(
|
||||
"mx-auto mb-3 h-10 w-10",
|
||||
isDark ? "text-cyan-500/50" : "text-slate-300 dark:text-slate-600"
|
||||
)}/>
|
||||
<p className={cn(
|
||||
isDark ? "uppercase tracking-wider font-mono" : "font-medium"
|
||||
)}>{title}</p>
|
||||
<Icon className="mx-auto mb-3 h-10 w-10 text-slate-300 dark:text-slate-600"/>
|
||||
<p className="font-medium">{title}</p>
|
||||
{subtitle && (
|
||||
<p className={cn(
|
||||
"mt-1 text-xs",
|
||||
isDark ? "text-cyan-500" : "text-slate-400 dark:text-slate-500"
|
||||
)}>
|
||||
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -14,41 +14,30 @@ type CustomTooltipProps = {
|
||||
};
|
||||
}>;
|
||||
label?: string | number;
|
||||
variant?: 'light' | 'dark';
|
||||
unit?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CustomTooltip = ({active, payload, label, variant = 'light', unit = '%'}: CustomTooltipProps) => {
|
||||
export const CustomTooltip = ({active, payload, label, unit = '%', className}: CustomTooltipProps) => {
|
||||
if (!active || !payload || payload.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDark = variant === 'dark';
|
||||
|
||||
// 从 payload 中获取完整的时间戳信息(如果有的话)
|
||||
const fullTimestamp = payload[0]?.payload?.timestamp;
|
||||
const displayLabel = fullTimestamp
|
||||
? dayjs(fullTimestamp).format(isDark ? 'YYYY-MM-DD HH:mm:ss' : 'MM-DD HH:mm')
|
||||
? dayjs(fullTimestamp).format('MM-DD HH:mm')
|
||||
: label;
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"rounded-md border px-3 py-2 text-xs shadow-xl",
|
||||
isDark
|
||||
? "border-cyan-700/50 bg-[#0a0b10]/95 backdrop-blur-sm"
|
||||
: "border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shadow-lg"
|
||||
"rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 shadow-lg px-3 py-2 text-xs",
|
||||
className
|
||||
)}>
|
||||
<p className={cn(
|
||||
"font-semibold",
|
||||
isDark
|
||||
? "font-mono text-cyan-300 text-xs tracking-wider uppercase"
|
||||
: "text-slate-700 dark:text-white mb-2"
|
||||
)}>
|
||||
<p className="font-semibold text-slate-700 dark:text-white mb-2">
|
||||
{displayLabel}
|
||||
</p>
|
||||
<div className={cn(
|
||||
isDark ? "mt-1 space-y-1" : "space-y-1"
|
||||
)}>
|
||||
<div className="space-y-1">
|
||||
{payload.map((entry, index) => {
|
||||
if (!entry) {
|
||||
return null;
|
||||
@@ -63,18 +52,13 @@ export const CustomTooltip = ({active, payload, label, variant = 'light', unit =
|
||||
: entry.value;
|
||||
|
||||
return (
|
||||
<p key={`${entry.dataKey ?? index}`} className={cn(
|
||||
"flex items-center gap-2",
|
||||
isDark ? "text-cyan-500" : "text-xs"
|
||||
)}>
|
||||
<p key={`${entry.dataKey ?? index}`} className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
style={{backgroundColor: dotColor}}
|
||||
/>
|
||||
<span className={isDark ? "" : "text-slate-600 dark:text-slate-400"}>
|
||||
{title}: <span className={cn(
|
||||
isDark ? "" : "font-semibold text-slate-900 dark:text-white"
|
||||
)}>{value}{unit}</span>
|
||||
<span className="text-slate-600 dark:text-slate-400">
|
||||
{title}: <span className="font-semibold text-slate-900 dark:text-white">{value}{unit}</span>
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import {AlertCircle, Server} from 'lucide-react';
|
||||
import {AlertCircle} from 'lucide-react';
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
interface EmptyStateProps {
|
||||
variant?: 'light' | 'dark';
|
||||
message?: string;
|
||||
showBackButton?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const EmptyState = ({variant = 'light', message, showBackButton = false}: EmptyStateProps) => {
|
||||
const isDark = variant === 'dark';
|
||||
const Icon = isDark ? Server : AlertCircle;
|
||||
const defaultMessage = isDark ? '服务器不存在或已离线' : '监控数据不存在';
|
||||
|
||||
export const EmptyState = ({message = '监控数据不存在', showBackButton = false, className}: EmptyStateProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex min-h-screen items-center justify-center",
|
||||
isDark ? "bg-[#05050a]" : "bg-slate-50 dark:bg-slate-900"
|
||||
"flex min-h-screen items-center justify-center bg-slate-50 dark:bg-slate-900",
|
||||
className
|
||||
)}>
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className={cn(
|
||||
"flex items-center justify-center",
|
||||
isDark
|
||||
? "h-16 w-16 rounded-lg bg-cyan-500/10 text-cyan-500"
|
||||
: "h-16 w-16 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500"
|
||||
)}>
|
||||
<Icon className="h-8 w-8"/>
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500">
|
||||
<AlertCircle className="h-8 w-8"/>
|
||||
</div>
|
||||
<p className={cn(
|
||||
"text-sm font-mono",
|
||||
isDark ? "text-cyan-500" : "text-slate-600 dark:text-slate-400"
|
||||
)}>
|
||||
{message || defaultMessage}
|
||||
<p className="text-sm font-mono text-slate-600 dark:text-slate-400">
|
||||
{message}
|
||||
</p>
|
||||
{showBackButton && !isDark && (
|
||||
{showBackButton && (
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="mt-4 text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
|
||||
@@ -2,28 +2,21 @@ import {Loader2} from 'lucide-react';
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
variant?: 'light' | 'dark';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const LoadingSpinner = ({variant = 'light', message}: LoadingSpinnerProps) => {
|
||||
const isDark = variant === 'dark';
|
||||
export const LoadingSpinner = ({message}: LoadingSpinnerProps) => {
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex min-h-screen items-center justify-center",
|
||||
isDark ? "bg-[#05050a]" : "bg-slate-50 dark:bg-slate-900"
|
||||
"flex h-[75vh] items-center justify-center bg-slate-50 dark:bg-[#05050a]",
|
||||
)}>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className={cn(
|
||||
"h-8 w-8 animate-spin",
|
||||
isDark ? "text-cyan-500" : "text-blue-500"
|
||||
)}/>
|
||||
<Loader2 className={"h-8 w-8 animate-spin text-blue-500 dark:text-cyan-500"}/>
|
||||
<p className={cn(
|
||||
"text-sm font-mono",
|
||||
isDark ? "text-cyan-500" : "text-slate-600 dark:text-slate-400"
|
||||
"text-sm font-mono text-slate-500 dark:text-cyan-400",
|
||||
)}>
|
||||
{message || (isDark ? '数据加载中...' : '数据加载中,请稍候...')}
|
||||
{message || '数据加载中,请稍候...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,22 +11,21 @@ interface TimeRangeSelectorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: readonly TimeRangeOption[];
|
||||
variant?: 'light' | 'dark';
|
||||
enableCustom?: boolean;
|
||||
customRange?: CustomRange | null;
|
||||
onCustomRangeApply?: (range: CustomRange) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TimeRangeSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
variant = 'light',
|
||||
enableCustom = false,
|
||||
customRange,
|
||||
onCustomRangeApply,
|
||||
className,
|
||||
}: TimeRangeSelectorProps) => {
|
||||
const isDark = variant === 'dark';
|
||||
const [customStart, setCustomStart] = useState('');
|
||||
const [customEnd, setCustomEnd] = useState('');
|
||||
|
||||
@@ -45,7 +44,7 @@ export const TimeRangeSelector = ({
|
||||
const showCustomOption = enableCustom && value === 'custom';
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
||||
<select
|
||||
value={showCustomOption ? 'custom' : value}
|
||||
onChange={(event) => {
|
||||
@@ -55,12 +54,7 @@ export const TimeRangeSelector = ({
|
||||
}
|
||||
onChange(nextValue);
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-all whitespace-nowrap",
|
||||
isDark
|
||||
? "border-cyan-900/50 bg-black/30 text-cyan-300 hover:border-cyan-700 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 font-mono"
|
||||
: "border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:border-blue-300 dark:hover:border-blue-600 focus:border-blue-400 dark:focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30"
|
||||
)}
|
||||
className="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:border-blue-300 dark:hover:border-blue-600 focus:border-blue-400 dark:focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30 px-3 py-1.5 text-xs font-medium transition-all whitespace-nowrap"
|
||||
>
|
||||
{showCustomOption && (
|
||||
<option value="custom" disabled>
|
||||
@@ -75,43 +69,23 @@ export const TimeRangeSelector = ({
|
||||
</select>
|
||||
{enableCustom && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
isDark ? "text-cyan-400" : "text-slate-600 dark:text-slate-300"
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium text-slate-600 dark:text-slate-300">
|
||||
自定义
|
||||
</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={customStart}
|
||||
onChange={(event) => setCustomStart(event.target.value)}
|
||||
className={cn(
|
||||
"rounded-lg border px-2 py-1 text-xs font-medium",
|
||||
isDark
|
||||
? "border-cyan-900/50 bg-black/30 text-cyan-300 focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20"
|
||||
: "border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30"
|
||||
)}
|
||||
className="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30 px-2 py-1 text-xs font-medium"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
isDark ? "text-cyan-400" : "text-slate-600 dark:text-slate-300"
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium text-slate-600 dark:text-slate-300">
|
||||
至
|
||||
</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={customEnd}
|
||||
onChange={(event) => setCustomEnd(event.target.value)}
|
||||
className={cn(
|
||||
"rounded-lg border px-2 py-1 text-xs font-medium",
|
||||
isDark
|
||||
? "border-cyan-900/50 bg-black/30 text-cyan-300 focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20"
|
||||
: "border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30"
|
||||
)}
|
||||
className="rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500/30 px-2 py-1 text-xs font-medium"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -123,14 +97,9 @@ export const TimeRangeSelector = ({
|
||||
disabled={!canApply}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-all whitespace-nowrap",
|
||||
isDark && "font-bold font-mono tracking-wider uppercase",
|
||||
canApply
|
||||
? isDark
|
||||
? "border-cyan-500/50 bg-cyan-500/20 text-cyan-300"
|
||||
: "border-blue-500 dark:border-blue-500 bg-blue-500 dark:bg-blue-600 text-white shadow-sm"
|
||||
: isDark
|
||||
? "border-cyan-900/30 bg-black/30 text-cyan-700 cursor-not-allowed"
|
||||
: "border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 cursor-not-allowed"
|
||||
? "border-blue-500 dark:border-blue-500 bg-blue-500 dark:bg-blue-600 text-white shadow-sm hover:bg-blue-600 dark:hover:bg-blue-700"
|
||||
: "border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
应用
|
||||
|
||||
@@ -3,78 +3,10 @@ import PublicHeader from '@portal/components/PublicHeader';
|
||||
import PublicFooter from '@portal/components/PublicFooter';
|
||||
import {ThemeProvider} from '../contexts/ThemeContext';
|
||||
|
||||
const globalStyles = `
|
||||
/* 1. 定义变量:默认亮色模式 */
|
||||
:root {
|
||||
--bg-color: #f0f2f5;
|
||||
--sb-track-color: #e5e7eb;
|
||||
--sb-thumb-color: #9ca3af;
|
||||
--sb-thumb-hover: #6b7280;
|
||||
--sb-thumb-border: #d1d5db;
|
||||
--sb-corner: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 2. 定义暗色模式下的变量值 */
|
||||
/* 只要 html 标签上有 class="dark" 就会生效 */
|
||||
html.dark {
|
||||
--bg-color: #05050a;
|
||||
--sb-track-color: #0a0a0f;
|
||||
--sb-thumb-color: #1e1e28;
|
||||
--sb-thumb-hover: #2a2a38;
|
||||
--sb-thumb-border: #2a2a35;
|
||||
--sb-corner: #05050a;
|
||||
}
|
||||
|
||||
/* 3. 应用背景色 */
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
/* 平滑过渡效果,可选 */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 4. Webkit 滚动条 (Chrome, Edge, Safari) - 只写一次 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--sb-track-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--sb-thumb-color);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--sb-thumb-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sb-thumb-hover);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--sb-corner);
|
||||
}
|
||||
|
||||
/* 5. Firefox 滚动条 - 只写一次 */
|
||||
html {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--sb-thumb-color) var(--sb-track-color);
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* 6. 防止滚动条消失时页面宽度变化 */
|
||||
body {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
`;
|
||||
|
||||
const PublicLayout = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="min-h-screen bg-[#f0f2f5] dark:bg-[#05050a] text-slate-800 dark:text-slate-200 flex flex-col relative overflow-x-hidden transition-colors duration-500">
|
||||
<style>{globalStyles}</style>
|
||||
<div className="bg-[#f0f2f5] dark:bg-[#05050a] text-slate-800 dark:text-slate-200 flex flex-col relative overflow-x-hidden transition-colors duration-500">
|
||||
{/* 背景网格效果 */}
|
||||
<div
|
||||
className="fixed inset-0 pointer-events-none z-0 transition-opacity duration-500"
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Globe,
|
||||
HardDrive,
|
||||
LinkIcon,
|
||||
Loader2,
|
||||
MemoryStick,
|
||||
Network,
|
||||
Thermometer,
|
||||
@@ -27,20 +26,12 @@ import ServerCard from "@portal/components/ServerCard.tsx";
|
||||
import NetworkStatCard from "@portal/components/NetworkStatCard.tsx";
|
||||
import {formatBytes, formatSpeed, formatUptime} from "@portal/utils/util.ts";
|
||||
import {isExpired} from "@portal/utils/server.ts";
|
||||
import {LoadingSpinner} from "@portal/components/LoadingSpinner.tsx";
|
||||
|
||||
interface AgentWithMetrics extends Agent {
|
||||
metrics?: LatestMetrics;
|
||||
}
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#f0f2f5] dark:bg-[#05050a]">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-cyan-500"/>
|
||||
<p className="text-sm text-cyan-500 font-mono">数据加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
@@ -22,7 +22,7 @@ const DDNSPage = lazy(() => import('@admin/pages/DDNS'));
|
||||
const AlertRecordListPage = lazy(() => import('@admin/pages/AlertRecords'));
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="flex min-h-[200px] w-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-[75vh] w-full items-center justify-center text-gray-500 dark:text-cyan-300">
|
||||
页面加载中...
|
||||
</div>
|
||||
);
|
||||
@@ -49,7 +49,7 @@ const router = createBrowserRouter([
|
||||
},
|
||||
// 公开页面 - 不需要登录
|
||||
{
|
||||
element: lazyLoad(PublicLayout),
|
||||
element: <PublicLayout/>,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
|
||||
Reference in new issue
Block a user