style: 🎨 Beautify dashboard & tunnel list with glassmorphism, background grids, and smoother transitions
This commit is contained in:
parent
3f2ecb89c0
commit
f20629f8d9
@ -1,38 +1,53 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Tailwind v4: class-based dark mode (set by next-themes on <html>) */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Design tokens */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-subtle: #f8fafc;
|
||||
--bg-card: #f1f5f9;
|
||||
--bg-card: #fdfdfd;
|
||||
--border: #e2e8f0;
|
||||
--border-subtle: #f1f5f9;
|
||||
--text: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--text-faint: #94a3b8;
|
||||
--accent: #10b981;
|
||||
--accent-dim: rgba(16,185,129,0.12);
|
||||
--accent-dim: rgba(16,185,129,0.08);
|
||||
--accent-border: rgba(16,185,129,0.3);
|
||||
--accent-glow: rgba(16,185,129,0.2);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--bg: #09090b;
|
||||
--bg-subtle: #111113;
|
||||
--bg-card: #18181b;
|
||||
--bg-card: #121214;
|
||||
--border: #27272a;
|
||||
--border-subtle: #1f1f22;
|
||||
--text: #f4f4f5;
|
||||
--text-muted: #71717a;
|
||||
--text-faint: #3f3f46;
|
||||
--text-muted: #a1a1aa;
|
||||
--text-faint: #52525b;
|
||||
--accent: #10b981;
|
||||
--accent-dim: rgba(16,185,129,0.1);
|
||||
--accent-border: rgba(16,185,129,0.25);
|
||||
--accent-glow: rgba(16,185,129,0.15);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@utility bg-grid {
|
||||
background-size: 32px 32px;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--border) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--border) 1px, transparent 1px);
|
||||
mask-image: radial-gradient(circle at top center, black, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(circle at top center, black, transparent 80%);
|
||||
}
|
||||
|
||||
@utility glass {
|
||||
background-color: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
@ -68,24 +68,25 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
||||
}, [apps, search, selectedTag]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--bg)]">
|
||||
<div className="min-h-screen bg-[var(--bg)] relative overflow-hidden">
|
||||
{/* Background accent */}
|
||||
<div className="fixed inset-0 -z-10 pointer-events-none">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,var(--accent-glow),transparent)]" />
|
||||
<div className="absolute inset-0 bg-grid opacity-30 dark:opacity-10" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 relative z-10">
|
||||
{/* Header */}
|
||||
<header className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)]">
|
||||
<Monitor className="w-5 h-5 text-emerald-500" />
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] shadow-sm shadow-[var(--accent-glow)]">
|
||||
<Monitor className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-[var(--text)]">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--text)]">
|
||||
SSH 管理台
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-muted)]">
|
||||
<p className="text-sm font-medium text-[var(--text-muted)] mt-0.5">
|
||||
Cloudflare Zero Trust · 浏览器终端
|
||||
</p>
|
||||
</div>
|
||||
@ -93,7 +94,7 @@ export default function Dashboard({ apps }: { apps: SshAppData[] }) {
|
||||
</header>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-10">
|
||||
<StatCard
|
||||
icon={<Server className="w-4 h-4" />}
|
||||
label="主机总数"
|
||||
@ -217,17 +218,22 @@ function StatCard({
|
||||
accent?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-4">
|
||||
<div className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white/50 dark:bg-zinc-900/50 backdrop-blur-xl p-5 shadow-sm transition-all hover:shadow-md hover:border-[var(--accent-border)]">
|
||||
{accent && (
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-[var(--accent-glow)] rounded-full blur-2xl -mr-10 -mt-10 pointer-events-none" />
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-xs mb-1 ${
|
||||
className={`flex items-center gap-2 text-sm font-medium mb-2 ${
|
||||
accent ? "text-emerald-500" : "text-[var(--text-muted)]"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
<div className={`p-1.5 rounded-lg ${accent ? 'bg-emerald-500/10' : 'bg-[var(--bg-subtle)]'}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xl font-bold tracking-tight ${
|
||||
className={`text-3xl font-extrabold tracking-tight ${
|
||||
accent ? "text-emerald-500" : "text-[var(--text)]"
|
||||
}`}
|
||||
>
|
||||
@ -239,45 +245,49 @@ function StatCard({
|
||||
|
||||
function HostCard({ app }: { app: SshAppData }) {
|
||||
return (
|
||||
<div className="group relative flex flex-col rounded-xl border border-[var(--border)] bg-[var(--bg-card)] transition-all duration-200 hover:border-[var(--accent-border)] hover:shadow-lg hover:shadow-[var(--accent-dim)] overflow-hidden">
|
||||
<a href={`/connect/${app.id}`} className="flex flex-col p-5 flex-1">
|
||||
<div className="group relative flex flex-col rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:border-emerald-500/50 hover:shadow-xl hover:shadow-[var(--accent-glow)] overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
<a href={`/connect/${app.id}`} className="flex flex-col p-6 flex-1 relative z-10">
|
||||
{/* Top row */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--accent-dim)] border border-[var(--accent-border)] shrink-0">
|
||||
<Terminal className="w-4 h-4 text-emerald-500" />
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-400/20 to-emerald-500/10 border border-emerald-500/30 shrink-0 shadow-inner group-hover:scale-105 transition-transform duration-300">
|
||||
<Terminal className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
<span className="font-semibold text-[var(--text)] truncate">{app.name}</span>
|
||||
<span className="font-bold text-[var(--text)] text-lg truncate tracking-tight">{app.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-[var(--bg-subtle)] border border-[var(--border)] group-hover:bg-emerald-500 group-hover:border-emerald-400 transition-colors duration-300 shrink-0">
|
||||
<ChevronRight className="w-4 h-4 text-[var(--text-muted)] group-hover:text-white group-hover:translate-x-0.5 transition-all duration-300" />
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-[var(--text-faint)] shrink-0 transition-all group-hover:text-emerald-500 group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
|
||||
{/* Domain */}
|
||||
<div className="flex items-center gap-1.5 text-sm text-[var(--text-muted)] mb-3 truncate">
|
||||
<Globe className="w-3.5 h-3.5 shrink-0 text-[var(--text-faint)]" />
|
||||
<span className="truncate">{app.domain}</span>
|
||||
<div className="flex items-center gap-2 text-[var(--text-muted)] mb-4 truncate bg-[var(--bg-subtle)] px-3 py-1.5 rounded-lg border border-[var(--border)] group-hover:border-[var(--text-faint)] transition-colors">
|
||||
<Globe className="w-4 h-4 shrink-0 text-[var(--text-faint)] group-hover:text-emerald-500/70 transition-colors" />
|
||||
<span className="truncate text-sm font-medium">{app.domain}</span>
|
||||
</div>
|
||||
|
||||
{/* Extra domains */}
|
||||
{app.domains.length > 1 && (
|
||||
<div className="text-xs text-[var(--text-faint)] mb-3">
|
||||
<div className="text-xs font-medium text-[var(--text-faint)] mb-4 flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--text-faint)]"></span>
|
||||
另有 {app.domains.length - 1} 个域名
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{app.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
<div className="flex flex-wrap gap-2 mb-4 mt-auto">
|
||||
{app.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-0.5 rounded-md bg-[var(--bg-subtle)] text-[var(--text-muted)] text-xs border border-[var(--border)]"
|
||||
className="px-2.5 py-1 rounded-lg bg-[var(--bg-subtle)] text-[var(--text-muted)] text-[11px] font-semibold tracking-wide uppercase border border-[var(--border)] group-hover:bg-[var(--bg-card)] transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{app.tags.length > 3 && (
|
||||
<span className="text-xs text-[var(--text-faint)]">
|
||||
<span className="px-2 py-1 text-[11px] font-bold text-[var(--text-faint)]">
|
||||
+{app.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
@ -285,20 +295,20 @@ function HostCard({ app }: { app: SshAppData }) {
|
||||
)}
|
||||
|
||||
{/* Bottom info */}
|
||||
<div className="mt-auto pt-3 border-t border-[var(--border)] flex items-center justify-between text-xs text-[var(--text-faint)]">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
<div className="mt-auto pt-4 border-t border-[var(--border)] flex items-center justify-between text-xs font-medium text-[var(--text-faint)]">
|
||||
<div className="flex items-center gap-1.5 group-hover:text-[var(--text-muted)] transition-colors">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
<span>{timeAgo(app.updatedAt)}</span>
|
||||
</div>
|
||||
{app.sessionDuration && (
|
||||
<span>TTL {app.sessionDuration}</span>
|
||||
<span className="px-2 py-0.5 rounded-md bg-[var(--bg-subtle)] border border-[var(--border)]">TTL {app.sessionDuration}</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Metrics widget (outside the connect link) */}
|
||||
{app.metricsUrl && (
|
||||
<div className="px-5 pb-5">
|
||||
<div className="px-6 pb-6 relative z-10">
|
||||
<MetricsWidget metricsUrl={app.metricsUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -56,41 +56,45 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-3.5rem)]">
|
||||
<div className="min-h-[calc(100vh-3.5rem)] bg-[var(--bg)] relative overflow-hidden">
|
||||
{/* Dynamic Backgrounds */}
|
||||
<div className="fixed inset-0 -z-10 pointer-events-none">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,var(--accent-dim),transparent)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,var(--accent-glow),transparent)]" />
|
||||
<div className="absolute inset-0 bg-grid opacity-30 dark:opacity-10" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-10 h-10 rounded-xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-emerald-500" />
|
||||
<div className="flex items-center gap-4 mb-10">
|
||||
<div className="w-12 h-12 rounded-2xl bg-[var(--accent-dim)] border border-[var(--accent-border)] flex items-center justify-center shadow-sm shadow-[var(--accent-glow)]">
|
||||
<Server className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text)]">隧道列表</h1>
|
||||
<p className="text-sm text-[var(--text-muted)]">
|
||||
共 {tunnels.length} 条 · {activeCount} 条在线
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-[var(--text)]">隧道列表</h1>
|
||||
<p className="text-sm font-medium text-[var(--text-muted)] mt-0.5">
|
||||
共 <span className="text-[var(--text)] font-bold">{tunnels.length}</span> 条 · <span className="text-emerald-500 font-bold">{activeCount}</span> 条在线
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative mb-6">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-faint)]" />
|
||||
<div className="relative mb-8">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-faint)]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="按名称或 ID 搜索..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] pl-10 pr-4 py-2.5 text-sm text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition focus:border-[var(--accent-border)] focus:ring-1 focus:ring-[var(--accent-dim)]"
|
||||
className="w-full rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl pl-11 pr-4 py-3.5 text-[var(--text)] placeholder:text-[var(--text-faint)] outline-none transition-all focus:border-[var(--accent-border)] focus:ring-2 focus:ring-[var(--accent-glow)] shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Empty */}
|
||||
{filtered.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-[var(--text-faint)]">
|
||||
<Server className="w-12 h-12 opacity-30 mb-4" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
|
||||
<Server className="w-8 h-8 opacity-30" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-[var(--text-muted)]">
|
||||
未找到隧道
|
||||
</p>
|
||||
@ -98,29 +102,36 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
)}
|
||||
|
||||
{/* Tunnel cards */}
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
{filtered.map((tunnel) => {
|
||||
const isActive =
|
||||
tunnel.connections && tunnel.connections.length > 0;
|
||||
return (
|
||||
<div
|
||||
key={tunnel.id}
|
||||
className="rounded-xl border border-[var(--border)] bg-[var(--bg-card)] p-5 transition hover:border-[var(--accent-border)]"
|
||||
className="group rounded-2xl border border-[var(--border)] bg-white/60 dark:bg-zinc-900/60 backdrop-blur-xl p-5 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg hover:border-emerald-500/30 overflow-hidden relative"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
)}
|
||||
|
||||
<div className="flex items-start justify-between gap-4 relative z-10">
|
||||
{/* Left */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="flex items-center gap-3 mb-1.5">
|
||||
{isActive ? (
|
||||
<Wifi className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
<div className="relative flex items-center justify-center w-5 h-5 shrink-0">
|
||||
<Wifi className="w-4 h-4 text-emerald-500 relative z-10" />
|
||||
<span className="absolute inset-0 rounded-full bg-emerald-500/30 animate-ping" />
|
||||
</div>
|
||||
) : (
|
||||
<WifiOff className="w-4 h-4 text-zinc-600 shrink-0" />
|
||||
<WifiOff className="w-4 h-4 text-zinc-500 shrink-0" />
|
||||
)}
|
||||
<span className="font-semibold text-[var(--text)] truncate">
|
||||
<span className="font-bold text-[var(--text)] text-lg truncate tracking-tight">
|
||||
{tunnel.name}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
className={`px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider ${
|
||||
isActive
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
|
||||
: "bg-[var(--bg-subtle)] text-[var(--text-faint)] border border-[var(--border)]"
|
||||
@ -129,15 +140,15 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
{isActive ? "在线" : "离线"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-faint)] font-mono truncate">
|
||||
<p className="text-sm text-[var(--text-muted)] font-mono truncate bg-[var(--bg-subtle)] inline-block px-2 py-0.5 rounded-md border border-[var(--border)]">
|
||||
{tunnel.id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right meta */}
|
||||
<div className="text-right text-xs text-[var(--text-faint)] shrink-0">
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<Clock className="w-3 h-3" />
|
||||
<div className="text-right text-xs font-medium text-[var(--text-faint)] shrink-0">
|
||||
<div className="flex items-center gap-1.5 justify-end bg-[var(--bg-subtle)] px-2.5 py-1 rounded-lg border border-[var(--border)]">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
创建于 {timeAgo(tunnel.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
@ -145,14 +156,14 @@ export default function TunnelList({ tunnels }: { tunnels: Tunnel[] }) {
|
||||
|
||||
{/* Connections */}
|
||||
{isActive && tunnel.connections.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--border)] flex flex-wrap gap-2">
|
||||
<div className="mt-4 pt-4 border-t border-[var(--border)] flex flex-wrap gap-2 relative z-10">
|
||||
{tunnel.connections.map((conn) => (
|
||||
<div
|
||||
key={conn.id}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-[var(--bg-subtle)] text-xs text-[var(--text-muted)]"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white/80 dark:bg-zinc-800/80 shadow-sm border border-[var(--border)] text-xs font-medium text-[var(--text-muted)] group-hover:border-[var(--text-faint)] transition-colors"
|
||||
>
|
||||
<MapPin className="w-3 h-3 text-[var(--text-faint)]" />
|
||||
{conn.origin_ip}
|
||||
<MapPin className="w-3.5 h-3.5 text-emerald-500/70" />
|
||||
<span className="text-[var(--text)]">{conn.origin_ip}</span>
|
||||
<span className="text-[var(--text-faint)]">·</span>
|
||||
<span className="text-[var(--text-faint)]">
|
||||
{timeAgo(conn.opened_at)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user