Files
Nexus/web/app/preview-v4.html
T
Nexus Deploy a2ae74d582 release: BUG fixes batch 1-6, full bug scan docs, Ubuntu/Linux dev
- Backend: auth refresh reuse, schedule/retry/sync/agent/files fixes
- Frontend: push WS, files ETag browse, vite build assets
- Docs: audit/changelog, production deploy gate, bug scan registry B7-77
- Deploy: deploy-production.sh, prune assets, gate logs
2026-06-04 14:01:14 +08:00

360 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Nexus Dashboard — Nuxt UI 风格</title>
<script src="/app/vendor/tailwindcss-browser.js"></script>
<style type="text/tailwindcss">
@theme {
--color-brand-50: #eff6ff; --color-brand-100: #dbeafe; --color-brand-200: #bfdbfe;
--color-brand-500: #3b82f6; --color-brand-600: #2563eb; --color-brand-700: #1d4ed8;
}
@custom-variant dark (&:where(.dark, .dark *));
</style>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root {
--ui-bg: #ffffff;
--ui-bg-elevated: oklch(96.7% .001 286.375);
--ui-bg-muted: oklch(98.5% 0 0);
--ui-bg-accented: oklch(92% .004 286.32);
--ui-border: oklch(92% .004 286.32);
--ui-border-muted: oklch(92% .004 286.32);
--ui-text: oklch(37% .013 285.805);
--ui-text-muted: oklch(55.2% .016 285.938);
--ui-text-dimmed: oklch(70.5% .015 286.067);
}
:root.dark {
--ui-bg: oklch(21% .006 285.885);
--ui-bg-elevated: oklch(27.4% .006 286.033);
--ui-bg-muted: oklch(27.4% .006 286.033);
--ui-bg-accented: oklch(37% .013 285.805);
--ui-border: oklch(27.4% .006 286.033);
--ui-border-muted: oklch(27.4% .006 286.033);
--ui-text: oklch(92% .004 286.32);
--ui-text-muted: oklch(70.5% .015 286.067);
--ui-text-dimmed: oklch(55.2% .016 285.938);
}
body {
background: var(--ui-bg); color: var(--ui-text);
font-family: 'Inter', -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
font-size: 14px; line-height: 1.5;
}
input,select,textarea,button{font-family:inherit}
.scrollbar::-webkit-scrollbar{width:4px}
.scrollbar::-webkit-scrollbar-track{background:transparent}
.scrollbar::-webkit-scrollbar-thumb{background:var(--ui-border);border-radius:2px}
/* ── Sidebar ── */
.nav-item {
display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:7px;
font-size:14px;font-weight:500;color:var(--ui-text-muted);
transition:all .15s;cursor:pointer;text-decoration:none;
}
.nav-item:hover{background:var(--ui-bg-elevated);color:var(--ui-text)}
.nav-item.active{background:var(--ui-bg-elevated);color:var(--ui-text);font-weight:600}
.nav-item svg{width:18px;height:18px}
/* ── Buttons (Nuxt UI style) ── */
.btn {
display:inline-flex;align-items:center;justify-content:center;gap:6px;height:36px;
padding:0 14px;border-radius:6px;font-size:13px;font-weight:500;
transition:all .15s;cursor:pointer;border:1px solid transparent;line-height:1;
}
.btn:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}
.btn-primary{background:#3b82f6;color:#fff;border-color:#3b82f6}.btn-primary:hover{background:#2563eb;border-color:#2563eb}
.btn-outline{background:transparent;color:var(--ui-text);border-color:var(--ui-border)}.btn-outline:hover{background:var(--ui-bg-elevated)}
.btn-ghost{background:transparent;color:var(--ui-text-muted);border-color:transparent}.btn-ghost:hover{background:var(--ui-bg-elevated);color:var(--ui-text)}
.btn-danger{background:#ef4444;color:#fff;border-color:#ef4444}.btn-danger:hover{background:#dc2626}
.btn-sm{height:30px;padding:0 10px;font-size:12px;border-radius:5px}
/* ── Inputs (Nuxt UI style) ── */
.input-wrap{position:relative}
.input {
height:34px;padding:0 11px;border-radius:6px;font-size:13px;border:1px solid var(--ui-border);
background:var(--ui-bg);color:var(--ui-text);outline:none;transition:all .15s;width:100%;
}
.input:hover{border-color:var(--ui-text-dimmed)}
.input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.12)}
.input::placeholder{color:var(--ui-text-dimmed)}
select.input{
-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%2370.5%25' stroke-width='2'%3E%3Cpath d='M2 5l4 4 4-4'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 10px center;padding-right:32px;
}
/* ── Cards (Nuxt UI style) ── */
.card {
background:var(--ui-bg-elevated);border:1px solid var(--ui-border);border-radius:6px;
padding:20px 24px;
}
.stat-card {
background:var(--ui-bg-elevated);border:1px solid var(--ui-border);border-radius:6px;
padding:24px;
}
/* ── Table (Nuxt UI style) ── */
.table-wrap {
border:1px solid var(--ui-border);border-radius:8px;overflow:hidden;
background:var(--ui-bg-elevated);
}
.table-wrap table{width:100%;border-collapse:collapse;font-size:13px}
.table-wrap thead{background:var(--ui-bg-muted)}
.table-wrap th {
padding:9px 14px;text-align:left;font-size:11px;font-weight:600;
text-transform:uppercase;letter-spacing:.04em;color:var(--ui-text-dimmed);
border-bottom:1px solid var(--ui-border);height:38px;vertical-align:middle;
}
.table-wrap td {
padding:11px 14px;border-bottom:1px solid var(--ui-border-muted);
color:var(--ui-text);vertical-align:middle;height:44px;
}
.table-wrap tr:last-child td{border-bottom:none}
.table-wrap tbody tr:hover{background:var(--ui-bg-accented)}
/* ── Status Badges ── */
.dot-green{width:6px;height:6px;border-radius:50%;background:#10b981;flex-shrink:0}
.dot-red{width:6px;height:6px;border-radius:50%;background:#ef4444;flex-shrink:0;animation:pulse 2s infinite}
.dot-gray{width:6px;height:6px;border-radius:50%;background:var(--ui-text-dimmed);flex-shrink:0}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.status-badge{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:500}
.status-text-green{color:#059669}.status-text-red{color:#dc2626}.status-text-gray{color:var(--ui-text-dimmed)}
.cell-label{display:inline-block;padding:1px 6px;border-radius:4px;font-size:11px;font-weight:500}
.cell-label-blue{background:rgba(59,130,246,.1);color:#3b82f6}.cell-label-gray{background:var(--ui-bg-muted);color:var(--ui-text-muted)}
.link{font-size:12px;color:var(--ui-text-muted);text-decoration:none;background:none;border:none;cursor:pointer;transition:.1s;padding:0;display:inline}
.link:hover{color:var(--ui-text)}
.link-primary{color:#3b82f6}.link-primary:hover{color:#1d4ed8}
.link-success{color:#059669}.link-success:hover{color:#047857}
.link-danger{color:#dc2626}.link-danger:hover{color:#b91c1c}
/* ── Pagination (Nuxt UI style) ── */
.pagination{display:flex;align-items:center;gap:2px;margin-top:14px}
.page-btn{min-width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:5px;border:1px solid transparent;background:transparent;color:var(--ui-text-muted);font-size:12px;cursor:pointer;transition:all .15s}
.page-btn:hover{background:var(--ui-bg-elevated);border-color:var(--ui-border)}
.page-btn.active{background:#3b82f6;color:#fff;border-color:#3b82f6;font-weight:600}
.page-btn:disabled{opacity:.3;cursor:default}
/* ── Tabs (Nuxt UI style - underline) ── */
.tab-bar{display:flex;gap:0}
.tab-btn{padding:8px 16px;font-size:13px;font-weight:500;color:var(--ui-text-muted);border-bottom:2px solid transparent;transition:all .15s;cursor:pointer;background:none;border-top:none;border-left:none;border-right:none}
.tab-btn:hover{color:var(--ui-text);border-bottom-color:var(--ui-border)}
.tab-btn.active{color:var(--ui-text);border-bottom-color:var(--ui-text);font-weight:600}
/* ── Modal (Nuxt UI style) ── */
.modal-overlay{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.4);backdrop-filter:blur(4px);opacity:0;visibility:hidden;transition:all .2s}
.modal-overlay.open{opacity:1;visibility:visible}
.modal{background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:8px;padding:20px;width:100%;max-width:420px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.15)}
/* ── Toast (Nuxt UI style) ── */
.toast{position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;max-width:380px;pointer-events:none}
.toast-item{pointer-events:auto;display:flex;align-items:flex-start;gap:10px;padding:12px 16px;border-radius:8px;font-size:13px;border:1px solid var(--ui-border);background:var(--ui-bg);color:var(--ui-text);box-shadow:0 4px 16px rgba(0,0,0,.1)}
.toast-success{border-left:3px solid #10b981}
.toast-error{border-left:3px solid #ef4444}
.toast-warning{border-left:3px solid #f59e0b}
/* ── Animations ── */
@keyframes slideIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
.slide-in{animation:slideIn .2s ease}
/* ── Separator ── */
.sep{height:1px;background:var(--ui-border);margin:12px 0;border:none}
/* ── Checkbox ── */
.form-check{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--ui-text);user-select:none}
.form-check input[type=checkbox]{width:15px;height:15px;border-radius:3px;accent-color:#3b82f6;cursor:pointer}
</style>
</head>
<body class="flex min-h-screen scrollbar">
<div class="toast" id="toastContainer"></div>
<!-- ════════ SIDEBAR ════════ -->
<aside class="w-60 shrink-0 flex flex-col border-r scrollbar" style="background:var(--ui-bg);border-color:var(--ui-border);overflow-y:auto">
<div class="px-4 py-3.5 border-b" style="border-color:var(--ui-border)">
<div class="flex items-center gap-2.5">
<svg class="w-6 h-6" style="color:#3b82f6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2"/></svg>
<span class="font-bold text-sm" style="color:var(--ui-text)">Nexus</span>
</div>
</div>
<nav class="flex-1 px-2.5 py-3 space-y-0.5">
<a class="nav-item active"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>仪表盘</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7"/></svg>服务器</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"/></svg>文件管理</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>推送</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>脚本库</a>
<div style="border-top:1px solid var(--ui-border);margin:6px 0"></div>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"/></svg>命令日志</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0"/></svg>告警中心</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192M5.801 8.248c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75A2.25 2.25 0 0010.25 7.5M8.25 8.25H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"/></svg>审计日志</a>
<a class="nav-item"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>设置</a>
</nav>
<div style="border-top:1px solid var(--ui-border);padding:10px 12px">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold" style="background:rgba(59,130,246,.1);color:#3b82f6">A</div>
<span class="text-xs" style="color:var(--ui-text-muted)">admin</span>
</div>
<button onclick="document.documentElement.classList.toggle('dark')" class="p-1 rounded hover:bg-[var(--ui-bg-elevated)] text-sm transition" style="color:var(--ui-text-muted)" title="切换主题">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"/></svg>
</button>
</div>
</div>
</aside>
<!-- ════════ MAIN ════════ -->
<div class="flex-1 flex flex-col min-w-0">
<header class="h-13 shrink-0 flex items-center justify-between px-6 border-b" style="background:var(--ui-bg);border-color:var(--ui-border)">
<h1 class="text-sm font-semibold" style="color:var(--ui-text)">仪表盘</h1>
<button class="btn btn-outline btn-sm" onclick="showToast('info','数据已刷新')">刷新</button>
</header>
<main class="flex-1 overflow-y-auto p-6 space-y-6 scrollbar">
<!-- Stats Cards -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="stat-card"><div class="text-[11px] font-semibold uppercase tracking-wide mb-3" style="color:var(--ui-text-dimmed)">服务器总数</div><div class="text-[32px] font-bold tracking-tight leading-none" style="color:var(--ui-text)">8</div></div>
<div class="stat-card"><div class="text-[11px] font-semibold uppercase tracking-wide mb-3" style="color:var(--ui-text-dimmed)">在线</div><div class="text-[32px] font-bold tracking-tight leading-none" style="color:#059669">1</div></div>
<div class="stat-card"><div class="text-[11px] font-semibold uppercase tracking-wide mb-3" style="color:var(--ui-text-dimmed)">离线</div><div class="text-[32px] font-bold tracking-tight leading-none" style="color:#dc2626">7</div></div>
<div class="stat-card"><div class="text-[11px] font-semibold uppercase tracking-wide mb-3" style="color:var(--ui-text-dimmed)">告警中</div><div class="text-[32px] font-bold tracking-tight leading-none" style="color:#d97706">0</div></div>
</div>
<!-- Table -->
<div>
<div class="flex items-center justify-between mb-3">
<h2 class="text-[13px] font-semibold" style="color:var(--ui-text)">服务器列表</h2>
<div class="flex items-center gap-2">
<select class="input" style="width:110px"><option>全部分类</option><option>商城</option><option>数据库</option></select>
<input class="input" style="width:180px" placeholder="搜索...">
<button class="btn btn-primary btn-sm">+ 添加</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>状态</th><th>名称</th><th>地址</th><th>分类</th><th>Agent</th><th>最后心跳</th><th style="text-align:right">操作</th></tr>
</thead>
<tbody>
<tr>
<td><span class="status-badge"><span class="dot-green"></span><span class="status-text-green">在线</span></span></td>
<td><span class="font-semibold">prod-web-01</span></td>
<td style="color:var(--ui-text-dimmed)">192.168.1.10<span style="color:var(--ui-border)">:22</span></td>
<td><span class="cell-label cell-label-blue">商城</span></td>
<td style="color:var(--ui-text-muted)">v1.2.0</td>
<td style="color:var(--ui-text-dimmed);font-size:12px">3分钟前</td>
<td style="text-align:right"><a href="#" class="link link-primary">终端</a> <a href="#" class="link">文件</a> <a href="#" class="link link-success">检查</a> <a href="#" class="link">编辑</a> <button class="link link-danger">删除</button></td>
</tr>
<tr>
<td><span class="status-badge"><span class="dot-red"></span><span class="status-text-red">离线</span></span></td>
<td><span class="font-semibold">dev-db-01</span></td>
<td style="color:var(--ui-text-dimmed)">10.0.0.5<span style="color:var(--ui-border)">:22</span></td>
<td style="color:var(--ui-text-dimmed)"></td>
<td style="color:var(--ui-text-muted)">v1.1.8</td>
<td style="color:var(--ui-text-dimmed);font-size:12px">2小时前</td>
<td style="text-align:right"><a href="#" class="link link-primary">终端</a> <a href="#" class="link">文件</a> <a href="#" class="link">编辑</a> <button class="link link-danger">删除</button></td>
</tr>
<tr>
<td><span class="status-badge"><span class="dot-gray"></span><span class="status-text-gray">未安装</span></span></td>
<td><span class="font-semibold">new-server-03</span></td>
<td style="color:var(--ui-text-dimmed)">172.16.0.8<span style="color:var(--ui-border)">:22</span></td>
<td style="color:var(--ui-text-dimmed)"></td>
<td style="color:var(--ui-text-dimmed)"></td>
<td style="color:var(--ui-text-dimmed);font-size:12px"></td>
<td style="text-align:right"><a href="#" class="link link-primary">终端</a> <a href="#" class="link">编辑</a> <button class="link link-danger">删除</button></td>
</tr>
<tr>
<td><span class="status-badge"><span class="dot-green"></span><span class="status-text-green">在线</span></span></td>
<td><span class="font-semibold">cache-redis-01</span></td>
<td style="color:var(--ui-text-dimmed)">10.0.0.10<span style="color:var(--ui-border)">:6379</span></td>
<td><span class="cell-label cell-label-blue">数据库</span></td>
<td style="color:var(--ui-text-muted)">v1.2.0</td>
<td style="color:var(--ui-text-dimmed);font-size:12px">1分钟前</td>
<td style="text-align:right"><a href="#" class="link link-primary">终端</a> <a href="#" class="link">文件</a> <a href="#" class="link link-success">检查</a> <a href="#" class="link">编辑</a> <button class="link link-danger">删除</button></td>
</tr>
<tr>
<td><span class="status-badge"><span class="dot-red"></span><span class="status-text-red">离线</span></td>
<td><span class="font-semibold">old-web-02</span></td>
<td style="color:var(--ui-text-dimmed)">192.168.1.20<span style="color:var(--ui-border)">:22</span></td>
<td><span class="cell-label cell-label-blue">商城</span></td>
<td style="color:var(--ui-text-dimmed)"></td>
<td style="color:var(--ui-text-dimmed);font-size:12px">3天前</td>
<td style="text-align:right"><a href="#" class="link link-primary">终端</a> <a href="#" class="link">编辑</a> <button class="link link-danger">删除</button></td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="text-xs" style="color:var(--ui-text-dimmed);margin-right:auto">共 8 台服务器</span>
<button class="page-btn" disabled></button>
<button class="page-btn active">1</button>
<button class="page-btn">2</button>
<button class="page-btn"></button>
</div>
</div>
<!-- Form + Component Showcase -->
<div class="grid grid-cols-2 gap-4">
<div class="card space-y-3" style="padding:20px">
<h3 class="text-[13px] font-semibold mb-1" style="color:var(--ui-text)">添加服务器</h3>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-[11px] font-semibold mb-1.5" style="color:var(--ui-text-muted)">名称</label><input class="input" placeholder="web-server-01"></div>
<div><label class="block text-[11px] font-semibold mb-1.5" style="color:var(--ui-text-muted)">地址</label><input class="input" placeholder="192.168.1.10"></div>
</div>
<div class="grid grid-cols-3 gap-3">
<div><label class="block text-[11px] font-semibold mb-1.5" style="color:var(--ui-text-muted)">端口</label><input class="input" value="22"></div>
<div><label class="block text-[11px] font-semibold mb-1.5" style="color:var(--ui-text-muted)">用户</label><input class="input" value="root"></div>
<div><label class="block text-[11px] font-semibold mb-1.5" style="color:var(--ui-text-muted)">认证</label><select class="input"><option>密码</option><option>密钥</option></select></div>
</div>
<div class="flex gap-2 pt-1"><button class="btn btn-primary btn-sm" onclick="showToast('success','服务器已添加')">保存</button><button class="btn btn-outline btn-sm">取消</button></div>
</div>
<div class="card space-y-3" style="padding:20px">
<h3 class="text-[13px] font-semibold mb-1" style="color:var(--ui-text)">组件</h3>
<div class="flex flex-wrap gap-2"><button class="btn btn-primary btn-sm">主要</button><button class="btn btn-outline btn-sm">次要</button><button class="btn btn-ghost btn-sm">幽灵</button><button class="btn btn-danger btn-sm">危险</button></div>
<hr class="sep">
<div class="tab-bar"><button class="tab-btn active">系统信息</button><button class="tab-btn">同步日志</button><button class="tab-btn">SSH 会话</button></div>
<hr class="sep">
<div class="flex gap-4">
<label class="form-check"><input type="checkbox" checked> 服务器 A</label>
<label class="form-check"><input type="checkbox"> 服务器 B</label>
</div>
<hr class="sep">
<div class="flex gap-2"><span class="status-badge"><span class="dot-green"></span><span class="status-text-green">在线</span></span><span class="status-badge"><span class="dot-red"></span><span class="status-text-red">离线</span></span><span class="status-badge"><span class="dot-gray"></span><span class="status-text-gray">未安装</span></span></div>
<hr class="sep">
<button class="btn btn-outline btn-sm" onclick="document.getElementById('demoModal').classList.add('open')">打开删除确认 Modal</button>
</div>
</div>
</main>
</div>
<!-- Modal -->
<div class="modal-overlay" id="demoModal" onclick="if(event.target===this)this.classList.remove('open')">
<div class="modal slide-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold" style="color:var(--ui-text)">确认删除</h3>
<button onclick="document.getElementById('demoModal').classList.remove('open')" style="background:none;border:none;cursor:pointer;color:var(--ui-text-dimmed);font-size:16px">&times;</button>
</div>
<p class="text-xs mb-6 leading-relaxed" style="color:var(--ui-text-muted)">确定要删除服务器 <strong style="color:var(--ui-text)">prod-web-01</strong> 吗?此操作无法撤销。</p>
<div class="flex justify-end gap-2">
<button class="btn btn-outline btn-sm" onclick="document.getElementById('demoModal').classList.remove('open')">取消</button>
<button class="btn btn-danger btn-sm" onclick="document.getElementById('demoModal').classList.remove('open');showToast('success','服务器已删除')">删除</button>
</div>
</div>
</div>
<script>
function showToast(type, msg) {
const icons = {success:'<svg style="width:15px;height:15px;shrink:0" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></svg>',error:'<svg style="width:15px;height:15px;shrink:0" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6m0-6l6 6"/></svg>',info:'<svg style="width:15px;height:15px;shrink:0" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>',warning:'<svg style="width:15px;height:15px;shrink:0" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><path d="M12 9v4m0 4h.01"/></svg>'}
const el = document.createElement('div');
el.className = 'toast-item toast-'+type+' slide-in';
el.innerHTML = (icons[type]||icons.info)+'<span style="flex:1">'+msg+'</span><button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:var(--ui-text-dimmed);font-size:16px;line-height:1">×</button>';
document.getElementById('toastContainer').appendChild(el);
setTimeout(()=>{el.style.opacity='0';el.style.transition='opacity .2s';setTimeout(()=>el.remove(),200)}, 3500);
}
</script>
</body>
</html>