优化前端代码

This commit is contained in:
dushixiang committed 2026-01-24 20:53:06 +08:00
1 parent a8d1d54c6a
commit a5ee886f2d
10 files changed
+54 -271

No files matched your search

+1 -52
View File
@@ -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;
}
/* 标签过滤器滚动条样式 */
+6 -17
View File
@@ -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>
)}
+8 -20
View File
@@ -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>
)}
+10 -26
View File
@@ -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>
);
+10 -22
View File
@@ -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"
+5 -12
View File
@@ -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>
+10 -41
View File
@@ -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"
)}
>
应用
+1 -69
View File
@@ -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"
+1 -10
View File
@@ -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;
+2 -2
View File
@@ -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: '/',