feat(portal): replace Sun-Panel with custom Apple Dark Glass HomeLab OS Portal + live telemetry API
This commit is contained in:
1 parent
5dafc94d5e
commit
a072a62141
3 files changed
+1025
-1
No files matched your search
@@ -0,0 +1,696 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>HomeLab OS · M6 All-in-One 中控台</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['-apple-system', 'BlinkMacSystemFont', 'SF Pro Display', 'Inter', 'PingFang SC', 'sans-serif'],
|
||||||
|
mono: ['SF Mono', 'JetBrains Mono', 'Menlo', 'monospace'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
border: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
surface: 'rgba(24, 24, 27, 0.65)',
|
||||||
|
surfaceHover: 'rgba(39, 39, 42, 0.75)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #09090b;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 15% 0%, rgba(56, 189, 248, 0.10) 0px, transparent 45%),
|
||||||
|
radial-gradient(at 85% 5%, rgba(139, 92, 246, 0.10) 0px, transparent 45%),
|
||||||
|
radial-gradient(at 50% 100%, rgba(16, 185, 129, 0.06) 0px, transparent 50%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #f4f4f5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(24, 24, 27, 0.62);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.glass-card-interactive {
|
||||||
|
transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.glass-card-interactive:hover {
|
||||||
|
background: rgba(39, 39, 42, 0.72);
|
||||||
|
border-color: rgba(255, 255, 255, 0.16);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.7), inset 0 1px 0 0 rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
[v-cloak] { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen selection:bg-sky-500/30 selection:text-sky-200">
|
||||||
|
<div id="app" v-cloak class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 space-y-6">
|
||||||
|
|
||||||
|
<!-- Top Toast Notification -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="transform -translate-y-4 opacity-0"
|
||||||
|
enter-to-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-to-class="transform -translate-y-4 opacity-0"
|
||||||
|
>
|
||||||
|
<div v-if="toast" class="fixed top-5 right-5 z-50 glass-card px-4 py-3 rounded-xl border border-emerald-500/30 flex items-center gap-3 shadow-2xl">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></span>
|
||||||
|
<span class="text-sm font-medium text-zinc-100">{{ toast }}</span>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Header / Navbar -->
|
||||||
|
<header class="glass-card rounded-2xl p-4 sm:px-6 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3.5">
|
||||||
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-sky-500 via-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/20 border border-white/20">
|
||||||
|
<svg class="w-5 h-5 text-white" 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-2m-2-4h.01M17 16h.01"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<h1 class="text-lg sm:text-xl font-semibold tracking-tight text-white">HomeLab OS</h1>
|
||||||
|
<span class="px-2 py-0.5 text-[11px] font-mono font-medium rounded-full bg-emerald-500/15 text-emerald-400 border border-emerald-500/25 flex items-center gap-1.5">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
4/4 节点在线
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-zinc-400 mt-0.5 font-mono">
|
||||||
|
Apple M6 Mac mini (192.168.31.164) ⇄ 极空间 Z4 (20TB) ⇄ SG Azure (104.43.105.243)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Controls: Mode Switcher & Embedded AGY Drawer Trigger -->
|
||||||
|
<div class="flex flex-wrap items-center gap-2.5">
|
||||||
|
<div class="bg-zinc-950/80 p-1 rounded-xl border border-white/10 flex items-center text-xs">
|
||||||
|
<button
|
||||||
|
v-for="m in modes"
|
||||||
|
:key="m.id"
|
||||||
|
@click="routeMode = m.id"
|
||||||
|
:class="[
|
||||||
|
'px-2.5 py-1.5 rounded-lg font-medium transition-all flex items-center gap-1.5',
|
||||||
|
routeMode === m.id
|
||||||
|
? 'bg-zinc-800 text-white shadow-sm border border-white/10'
|
||||||
|
: 'text-zinc-400 hover:text-zinc-200'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span>{{ m.icon }}</span>
|
||||||
|
<span>{{ m.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showAgyDrawer = true"
|
||||||
|
class="px-3.5 py-2 rounded-xl text-xs font-medium bg-gradient-to-r from-sky-500/20 to-indigo-500/20 hover:from-sky-500/30 hover:to-indigo-500/30 text-sky-300 border border-sky-500/30 transition-all flex items-center gap-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="w-2 h-2 rounded-full bg-sky-400 animate-ping"></span>
|
||||||
|
<span>页内唤起 AGY 终端</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Bento Grid: 3 Real-Time Telemetry Cards -->
|
||||||
|
<section class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
|
||||||
|
<!-- 1. M6 Mac mini Telemetry -->
|
||||||
|
<div class="glass-card rounded-2xl p-5 flex flex-col justify-between space-y-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs font-mono uppercase tracking-wider text-sky-400 font-semibold">Core Compute Server</span>
|
||||||
|
<span class="px-2 py-0.5 text-[10px] font-mono rounded-md bg-zinc-800/90 text-zinc-300 border border-white/5">
|
||||||
|
UP {{ telemetry.mini.uptime }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-base font-semibold text-white mt-1 flex items-center gap-2">
|
||||||
|
<span>🖥️ Mac mini · {{ telemetry.mini.chip }}</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-zinc-400 bg-zinc-900/90 px-2.5 py-1 rounded-lg border border-white/5">
|
||||||
|
{{ telemetry.mini.ip }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resource Bars -->
|
||||||
|
<div class="space-y-3 pt-1">
|
||||||
|
<!-- CPU -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-zinc-400">CPU 负载 (12核 M6 · Load {{ telemetry.mini.load_1m }})</span>
|
||||||
|
<span class="font-mono text-zinc-200 font-medium">{{ telemetry.mini.cpu_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2 bg-zinc-900 rounded-full overflow-hidden border border-white/5">
|
||||||
|
<div class="h-full bg-gradient-to-r from-sky-500 to-indigo-500 rounded-full progress-bar" :style="{ width: Math.max(4, telemetry.mini.cpu_pct) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RAM -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-zinc-400">统一内存 ({{ telemetry.mini.mem_used_gb }} / {{ telemetry.mini.mem_total_gb }} GB)</span>
|
||||||
|
<span class="font-mono text-zinc-200 font-medium">{{ telemetry.mini.mem_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2 bg-zinc-900 rounded-full overflow-hidden border border-white/5">
|
||||||
|
<div class="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full progress-bar" :style="{ width: Math.max(4, telemetry.mini.mem_pct) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SSD -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-zinc-400">系统闪存 ({{ telemetry.mini.disk_used_gb }} / {{ telemetry.mini.disk_total_gb }} GB)</span>
|
||||||
|
<span class="font-mono text-zinc-200 font-medium">{{ telemetry.mini.disk_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2 bg-zinc-900 rounded-full overflow-hidden border border-white/5">
|
||||||
|
<div class="h-full bg-gradient-to-r from-emerald-500 to-teal-400 rounded-full progress-bar" :style="{ width: Math.max(4, telemetry.mini.disk_pct) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Pills -->
|
||||||
|
<div class="pt-2 border-t border-white/5 flex flex-wrap items-center justify-between gap-2 text-[11px] font-mono text-zinc-400">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||||
|
<span>AGY tmux: {{ telemetry.mini.agy_tmux ? '常驻运行中' : '待机' }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
OrbStack 容器: <span class="text-zinc-200 font-semibold">{{ telemetry.mini.containers.length }} 运行中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. ZSpace Z4 20TB Storage Telemetry -->
|
||||||
|
<div class="glass-card rounded-2xl p-5 flex flex-col justify-between space-y-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs font-mono uppercase tracking-wider text-emerald-400 font-semibold">Storage Pool & Backup</span>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'px-2 py-0.5 text-[10px] font-mono rounded-md border',
|
||||||
|
telemetry.nas.mounted
|
||||||
|
? 'bg-emerald-500/15 text-emerald-300 border-emerald-500/25'
|
||||||
|
: 'bg-amber-500/15 text-amber-300 border-amber-500/25'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ telemetry.nas.mounted ? 'SMB 已挂载 ~/nas' : '未挂载' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-base font-semibold text-white mt-1">
|
||||||
|
💾 极空间 Z4 · 20TB 存储中心
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-zinc-400 bg-zinc-900/90 px-2.5 py-1 rounded-lg border border-white/5">
|
||||||
|
{{ telemetry.nas.ip }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Big Storage Meter -->
|
||||||
|
<div class="bg-zinc-950/60 rounded-xl p-3.5 border border-white/5 space-y-2.5">
|
||||||
|
<div class="flex items-baseline justify-between">
|
||||||
|
<div>
|
||||||
|
<span class="text-2xl font-bold tracking-tight text-white font-mono">{{ telemetry.nas.free_tb }} TB</span>
|
||||||
|
<span class="text-xs text-zinc-400 ml-1.5">剩余可用</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-zinc-400">
|
||||||
|
已用 {{ telemetry.nas.used_tb }} TB / 总计 {{ telemetry.nas.total_tb }} TB ({{ telemetry.nas.pct }}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-2.5 bg-zinc-900 rounded-full overflow-hidden border border-white/5">
|
||||||
|
<div class="h-full bg-gradient-to-r from-emerald-500 via-teal-400 to-cyan-400 rounded-full progress-bar" :style="{ width: Math.max(4, telemetry.nas.pct) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-[11px] text-zinc-400 font-mono pt-0.5">
|
||||||
|
<span>挂载点: ~/nas/{共享文件,个人空间}</span>
|
||||||
|
<span>Time Machine: 手动按需</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backup Action Row -->
|
||||||
|
<div class="pt-2 border-t border-white/5 flex items-center justify-between gap-2">
|
||||||
|
<div class="text-[11px] text-zinc-400 truncate">
|
||||||
|
最近自动备份: <span class="font-mono text-zinc-200">{{ telemetry.nas.latest_backup }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="triggerAction('backup_nas', '正在打包备份至极空间 Z4...')"
|
||||||
|
:disabled="actionLoading"
|
||||||
|
class="px-3 py-1.5 rounded-lg text-xs font-medium bg-zinc-800 hover:bg-zinc-700 text-zinc-100 border border-white/10 transition-all shrink-0 flex items-center gap-1.5 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span>⚡ 立即备份</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. AI Routing & SG Cloud Edge Telemetry -->
|
||||||
|
<div class="glass-card rounded-2xl p-5 flex flex-col justify-between space-y-4">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs font-mono uppercase tracking-wider text-purple-400 font-semibold">AI Egress & Cloud Edge</span>
|
||||||
|
<span class="px-2 py-0.5 text-[10px] font-mono rounded-md bg-purple-500/15 text-purple-300 border border-purple-500/25">
|
||||||
|
SG 出口死锁保护
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-base font-semibold text-white mt-1">
|
||||||
|
🛡️ 小米主路由 ⇄ 新加坡 Azure
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-lg border border-emerald-500/20">
|
||||||
|
{{ telemetry.network.sg_delay_ms > 0 ? telemetry.network.sg_delay_ms + ' ms' : '专线直连' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network Rows -->
|
||||||
|
<div class="space-y-2 text-xs font-mono">
|
||||||
|
<div class="flex items-center justify-between bg-zinc-950/60 px-3 py-2 rounded-xl border border-white/5">
|
||||||
|
<span class="text-zinc-400">AGY / AI 分流策略</span>
|
||||||
|
<span class="text-purple-300 font-medium">{{ telemetry.network.ai_policy }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between bg-zinc-950/60 px-3 py-2 rounded-xl border border-white/5">
|
||||||
|
<span class="text-zinc-400">AI 锁定公网出口 (SG)</span>
|
||||||
|
<span class="text-zinc-100">{{ telemetry.network.sg_egress_ip }} ({{ telemetry.network.sg_node }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between bg-zinc-950/60 px-3 py-2 rounded-xl border border-white/5">
|
||||||
|
<span class="text-zinc-400">家庭联通公网 IPv4</span>
|
||||||
|
<span class="text-sky-300">{{ telemetry.network.home_wan_ip }} (*.home.961504.xyz)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="pt-2 border-t border-white/5 flex items-center justify-between text-[11px] text-zinc-400">
|
||||||
|
<span class="font-mono">Tailscale P2P: 100.108.205.40 ⇄ 100.115.204.51</span>
|
||||||
|
<button
|
||||||
|
@click="triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...')"
|
||||||
|
:disabled="actionLoading"
|
||||||
|
class="px-2.5 py-1 rounded-lg text-xs font-medium bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-white/10 transition-all"
|
||||||
|
>
|
||||||
|
刷新 DDNS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main Services Launchpad Grid -->
|
||||||
|
<section class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between px-1">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wider text-zinc-400">全屋核心服务矩阵 (Service Launchpad)</h2>
|
||||||
|
<span class="text-xs text-zinc-500 font-mono">
|
||||||
|
当前点击跳转模式: {{ activeModeLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-zinc-500 hidden sm:inline">所有受保护服务统一账号: <code class="text-zinc-300 font-mono">ixvii0iv</code></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<a
|
||||||
|
v-for="svc in services"
|
||||||
|
:key="svc.id"
|
||||||
|
:href="resolveUrl(svc)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="glass-card glass-card-interactive rounded-2xl p-5 flex flex-col justify-between group cursor-pointer relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Subtle Top Gradient Glow -->
|
||||||
|
<div :class="['absolute -right-10 -top-10 w-28 h-28 rounded-full blur-2xl opacity-20 group-hover:opacity-40 transition-opacity', svc.glow]"></div>
|
||||||
|
|
||||||
|
<div class="space-y-3 relative z-10">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div :class="['w-11 h-11 rounded-xl flex items-center justify-center text-xl border border-white/15 shadow-inner', svc.iconBg]">
|
||||||
|
{{ svc.emoji }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3 class="text-base font-semibold text-white group-hover:text-sky-300 transition-colors">
|
||||||
|
{{ svc.title }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
telemetry.services[svc.healthKey] !== false ? 'bg-emerald-400 shadow-[0_0_8px_#34d399]' : 'bg-rose-500'
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[11px] font-mono text-zinc-400">{{ svc.badge }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-zinc-500 group-hover:text-zinc-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 17L17 7M17 7H7M17 7V17"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-zinc-400 leading-relaxed">
|
||||||
|
{{ svc.desc }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-3.5 mt-4 border-t border-white/5 flex items-center justify-between text-[11px] font-mono relative z-10">
|
||||||
|
<span class="text-zinc-400 truncate max-w-[72%]">{{ resolveUrl(svc) }}</span>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
v-if="svc.id === 'agy'"
|
||||||
|
@click.prevent="showAgyDrawer = true"
|
||||||
|
class="px-2 py-0.5 rounded bg-sky-500/20 text-sky-300 border border-sky-500/30 hover:bg-sky-500/30"
|
||||||
|
>
|
||||||
|
页内打开
|
||||||
|
</button>
|
||||||
|
<span v-else class="px-2 py-0.5 rounded bg-zinc-800/80 text-zinc-300 border border-white/5">
|
||||||
|
{{ resolveTag(svc) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SSH & Terminal Quick Commands Bar -->
|
||||||
|
<section class="glass-card rounded-2xl p-4 sm:px-6 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-2 text-xs text-zinc-400">
|
||||||
|
<span class="text-zinc-200 font-semibold">⌨️ 终端快捷指令 (点击即复制):</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
v-for="cmd in sshCommands"
|
||||||
|
:key="cmd.cmd"
|
||||||
|
@click="copyText(cmd.cmd, cmd.label)"
|
||||||
|
class="px-3 py-1.5 rounded-xl bg-zinc-950/80 hover:bg-zinc-800/90 border border-white/10 text-xs font-mono text-zinc-300 hover:text-white transition-all flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-zinc-500">{{ cmd.label }}:</span>
|
||||||
|
<span class="text-sky-300">{{ cmd.cmd }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Slide-over AGY Cloud Terminal Drawer / Modal -->
|
||||||
|
<div v-if="showAgyDrawer" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-2 sm:p-6">
|
||||||
|
<div class="w-full h-full max-w-6xl bg-zinc-950 rounded-2xl border border-white/15 shadow-2xl flex flex-col overflow-hidden">
|
||||||
|
<div class="px-4 py-3 bg-zinc-900/90 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<button @click="showAgyDrawer = false" class="w-3 h-3 rounded-full bg-rose-500 hover:opacity-80"></button>
|
||||||
|
<span class="w-3 h-3 rounded-full bg-amber-500"></span>
|
||||||
|
<a :href="agyIframeUrl" target="_blank" class="w-3 h-3 rounded-full bg-emerald-500 hover:opacity-80"></a>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-mono text-zinc-200 font-medium">
|
||||||
|
🤖 AGY 云端工作台 (tmux attach -t agy · 关窗不断连)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
:href="agyIframeUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="px-2.5 py-1 rounded-lg text-xs font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-200 border border-white/10"
|
||||||
|
>
|
||||||
|
↗ 新标签页全屏打开
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="showAgyDrawer = false"
|
||||||
|
class="px-2.5 py-1 rounded-lg text-xs bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 border border-rose-500/30"
|
||||||
|
>
|
||||||
|
关闭窗口 (后台继续运行)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<iframe :src="agyIframeUrl" class="w-full flex-1 bg-black border-0"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, computed, onMounted } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
setup() {
|
||||||
|
const routeMode = ref('auto');
|
||||||
|
const showAgyDrawer = ref(false);
|
||||||
|
const toast = ref('');
|
||||||
|
const actionLoading = ref(false);
|
||||||
|
|
||||||
|
const modes = [
|
||||||
|
{ id: 'auto', icon: '⚡', label: '智能自动' },
|
||||||
|
{ id: 'edge443', icon: '🌐', label: '443 免端口' },
|
||||||
|
{ id: 'home10443', icon: '🏠', label: '10443 家宽' },
|
||||||
|
{ id: 'lan', icon: '🔌', label: '局域网 LAN' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const telemetry = ref({
|
||||||
|
mini: {
|
||||||
|
chip: 'Apple M6 (12核)',
|
||||||
|
ip: '192.168.31.164',
|
||||||
|
cpu_pct: 5.2,
|
||||||
|
load_1m: 0.65,
|
||||||
|
mem_used_gb: 6.4,
|
||||||
|
mem_total_gb: 24.0,
|
||||||
|
mem_pct: 26.7,
|
||||||
|
disk_used_gb: 28.5,
|
||||||
|
disk_total_gb: 460.4,
|
||||||
|
disk_pct: 6.2,
|
||||||
|
uptime: '运行中',
|
||||||
|
containers: [{ name: 'vaultwarden' }],
|
||||||
|
agy_tmux: true,
|
||||||
|
},
|
||||||
|
nas: {
|
||||||
|
name: '极空间 Z4',
|
||||||
|
ip: '192.168.31.185',
|
||||||
|
online: true,
|
||||||
|
mounted: true,
|
||||||
|
total_tb: 20.0,
|
||||||
|
used_tb: 2.9,
|
||||||
|
free_tb: 17.1,
|
||||||
|
pct: 14.5,
|
||||||
|
latest_backup: '已就绪',
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
home_wan_ip: '124.90.142.21',
|
||||||
|
ai_policy: 'Ai平台 → 狮城节点',
|
||||||
|
sg_node: 'coco-sea1-reality-sony',
|
||||||
|
sg_egress_ip: '104.43.105.243',
|
||||||
|
sg_delay_ms: 48,
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
agy: true,
|
||||||
|
vaultwarden: true,
|
||||||
|
nas: true,
|
||||||
|
clash: true,
|
||||||
|
router: true,
|
||||||
|
uptime: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
id: 'agy',
|
||||||
|
healthKey: 'agy',
|
||||||
|
emoji: '🤖',
|
||||||
|
title: 'AGY 云端工作台',
|
||||||
|
badge: 'ttyd + tmux 24h 常驻终端',
|
||||||
|
desc: 'Mac mini 上的 Antigravity AI 编程终端,已同步 Google 登录态并锁定新加坡固定出口,关掉网页任务继续在后台跑。',
|
||||||
|
edgeUrl: 'https://agy.961504.xyz',
|
||||||
|
homeUrl: 'https://agy.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:7681',
|
||||||
|
iconBg: 'bg-sky-500/20 text-sky-300',
|
||||||
|
glow: 'bg-sky-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zspace',
|
||||||
|
healthKey: 'nas',
|
||||||
|
emoji: '💾',
|
||||||
|
title: '极空间 Z4 私有云',
|
||||||
|
badge: '20TB RAID 存储池 · 极影视',
|
||||||
|
desc: '家庭 20TB 核心大容量存储、极影视蓝光家庭影院、手机相册备份中心,已自动 SMB 挂载至 Mac mini ~/nas。',
|
||||||
|
edgeUrl: 'https://z4.home.961504.xyz:10443',
|
||||||
|
homeUrl: 'https://z4.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.185:5055',
|
||||||
|
iconBg: 'bg-emerald-500/20 text-emerald-300',
|
||||||
|
glow: 'bg-emerald-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vaultwarden',
|
||||||
|
healthKey: 'vaultwarden',
|
||||||
|
emoji: '🔐',
|
||||||
|
title: 'Vaultwarden 密码保险箱',
|
||||||
|
badge: 'Bitwarden 官方客户端兼容',
|
||||||
|
desc: '自托管全平台加密密码库,支持 iOS / Android / Chrome 自动填充,每天凌晨 03:30 自动打包灾备至极空间 Z4。',
|
||||||
|
edgeUrl: 'https://vw.961504.xyz',
|
||||||
|
homeUrl: 'https://vw.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:8222',
|
||||||
|
iconBg: 'bg-indigo-500/20 text-indigo-300',
|
||||||
|
glow: 'bg-indigo-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clash',
|
||||||
|
healthKey: 'clash',
|
||||||
|
emoji: '🚀',
|
||||||
|
title: 'Mihomo 路由分流面板',
|
||||||
|
badge: 'ShellCrash 透明网关控制台',
|
||||||
|
desc: '小米主路由器实时流量监控、策略组切换与规则明细(Ai平台已死锁狮城节点 104.43.105.243)。',
|
||||||
|
edgeUrl: 'https://clash.home.961504.xyz:10443/ui/',
|
||||||
|
homeUrl: 'https://clash.home.961504.xyz:10443/ui/',
|
||||||
|
lanUrl: 'http://192.168.31.1:9999/ui/',
|
||||||
|
iconBg: 'bg-purple-500/20 text-purple-300',
|
||||||
|
glow: 'bg-purple-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'router',
|
||||||
|
healthKey: 'router',
|
||||||
|
emoji: '📡',
|
||||||
|
title: '小米主路由器后台',
|
||||||
|
badge: '192.168.31.1 · PPPoE & DNAT',
|
||||||
|
desc: '管理家庭联通宽带拨号、DHCP 静态 IP 绑定、10443 与 41641 端口映射及全屋 Wi-Fi 设备状态。',
|
||||||
|
edgeUrl: 'https://router.home.961504.xyz:10443',
|
||||||
|
homeUrl: 'https://router.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.1',
|
||||||
|
iconBg: 'bg-amber-500/20 text-amber-300',
|
||||||
|
glow: 'bg-amber-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uptime',
|
||||||
|
healthKey: 'uptime',
|
||||||
|
emoji: '📈',
|
||||||
|
title: 'Uptime Kuma 外部哨兵',
|
||||||
|
badge: 'SG Azure 104.43.105.243',
|
||||||
|
desc: '独立部署在新加坡公有云上的 24 小时外部监控哨兵,从家庭外部实时探测家里宽带与各项服务在线率。',
|
||||||
|
edgeUrl: 'https://status.961504.xyz',
|
||||||
|
homeUrl: 'https://status.961504.xyz',
|
||||||
|
lanUrl: 'https://status.961504.xyz',
|
||||||
|
iconBg: 'bg-teal-500/20 text-teal-300',
|
||||||
|
glow: 'bg-teal-500',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sshCommands = [
|
||||||
|
{ label: '连 Mac mini', cmd: 'ssh mini' },
|
||||||
|
{ label: '接管 AGY 会话', cmd: 'ssh mini -t "tmux attach -t agy"' },
|
||||||
|
{ label: '连新加坡 Azure', cmd: 'ssh azure' },
|
||||||
|
{ label: '连小米路由器', cmd: 'ssh root@192.168.31.1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const effectiveMode = computed(() => {
|
||||||
|
if (routeMode.value !== 'auto') return routeMode.value;
|
||||||
|
const host = window.location.hostname;
|
||||||
|
if (host.startsWith('192.168.') || host.startsWith('100.') || host === 'localhost') {
|
||||||
|
return 'lan';
|
||||||
|
}
|
||||||
|
if (host.includes('.home.961504.xyz') || window.location.port === '10443') {
|
||||||
|
return 'home10443';
|
||||||
|
}
|
||||||
|
return 'edge443';
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeModeLabel = computed(() => {
|
||||||
|
const m = effectiveMode.value;
|
||||||
|
if (m === 'lan') return '局域网超低延迟直连 (192.168.31.x)';
|
||||||
|
if (m === 'home10443') return '家宽公网 HTTPS 直连 (*.home.961504.xyz:10443)';
|
||||||
|
return '443 免端口 HTTPS 直达 (*.961504.xyz)';
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolveUrl = (svc) => {
|
||||||
|
const m = effectiveMode.value;
|
||||||
|
if (m === 'lan') return svc.lanUrl;
|
||||||
|
if (m === 'home10443') return svc.homeUrl;
|
||||||
|
return svc.edgeUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTag = (svc) => {
|
||||||
|
const m = effectiveMode.value;
|
||||||
|
if (m === 'lan') return 'LAN 极速';
|
||||||
|
if (m === 'home10443') return '10443 直连';
|
||||||
|
return '443 免端口';
|
||||||
|
};
|
||||||
|
|
||||||
|
const agyIframeUrl = computed(() => {
|
||||||
|
return resolveUrl(services[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showToast = (msg) => {
|
||||||
|
toast.value = msg;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.value === msg) toast.value = '';
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyText = async (text, label) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast(`已复制「${label}」指令: ${text}`);
|
||||||
|
} catch {
|
||||||
|
showToast(`指令: ${text}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchTelemetry = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/telemetry');
|
||||||
|
if (res.ok) {
|
||||||
|
telemetry.value = await res.json();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAction = async (action, startMsg) => {
|
||||||
|
actionLoading.value = true;
|
||||||
|
showToast(startMsg);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/action/${action}`, { method: 'POST' });
|
||||||
|
const data = await res.json();
|
||||||
|
showToast(data.message || '操作成功');
|
||||||
|
await fetchTelemetry();
|
||||||
|
} catch (e) {
|
||||||
|
showToast('执行失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
actionLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTelemetry();
|
||||||
|
setInterval(fetchTelemetry, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
modes,
|
||||||
|
routeMode,
|
||||||
|
effectiveMode,
|
||||||
|
activeModeLabel,
|
||||||
|
telemetry,
|
||||||
|
services,
|
||||||
|
sshCommands,
|
||||||
|
showAgyDrawer,
|
||||||
|
agyIframeUrl,
|
||||||
|
toast,
|
||||||
|
actionLoading,
|
||||||
|
resolveUrl,
|
||||||
|
resolveTag,
|
||||||
|
copyText,
|
||||||
|
triggerAction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user