revert(portal): restore V3 colorful icon layout on 3004

Browse Source
This commit is contained in:
Yin Kou committed 2026-11-06 23:51:39 +08:00
1 parent 3ddc18c048
commit daddff375c
1 file changed
+307 -221
+307 -221
View File
@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HOMELAB // CONSOLE</title> <title>HomeLab Console · 家庭云边中控台</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
@@ -12,53 +13,99 @@
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<style> <style>
/* ========================================================================= /* =========================================================================
Pure Monochrome Precision Design System (Vercel Geist / Linear / Apple Pro) HomeLab Console V3
ZERO cartoon logos, ZERO random blue/purple/green text splashes. Architecture: gethomepage.dev (Top Ribbon + 3-Col Rows + Inline Widgets)
100% Carbon, Obsidian, Graphite, Hairline Zinc & Platinum White. Design System: shadcn/ui + Raycast Command Palette + 4 Official Themes
========================================================================= */ ========================================================================= */
:root, [data-theme="dark"] { :root, [data-theme="zinc-dark"] {
--bg-page: #080809; --bg-page: #09090b;
--bg-card: #0f0f11; --bg-card: #121215;
--bg-widget: #0b0b0d; --bg-widget: #16161a;
--bg-muted: #18181b; --bg-muted: #18181b;
--bg-hover: #141417; --bg-hover: #1c1c21;
--text-main: #f4f4f5; --text-main: #f4f4f5;
--text-secondary: #d4d4d8; --text-muted: #a1a1aa;
--text-muted: #8e8e93; --text-subtle: #71717a;
--text-subtle: #52525b; --border-color: #27272a;
--border-color: #222226; --border-subtle: rgba(255, 255, 255, 0.06);
--border-hover: #3f3f46; --border-hover: #3f3f46;
--primary: #f4f4f5; --primary: #fafafa;
--primary-fg: #09090b; --primary-fg: #09090b;
--bar-fill: #e4e4e7; --accent: #3b82f6;
--dot-online: #e4e4e7; --accent-soft: rgba(59, 130, 246, 0.14);
--card-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.7); --success: #10b981;
--success-soft: rgba(16, 185, 129, 0.14);
--card-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.5);
} }
[data-theme="light"] { [data-theme="zinc-light"] {
--bg-page: #f5f5f7; --bg-page: #f8fafc;
--bg-card: #ffffff; --bg-card: #ffffff;
--bg-widget: #fafafa; --bg-widget: #f8fafc;
--bg-muted: #ececef; --bg-muted: #f1f5f9;
--bg-hover: #ffffff; --bg-hover: #ffffff;
--text-main: #111113; --text-main: #0f172a;
--text-secondary: #27272a; --text-muted: #64748b;
--text-muted: #6e6e73; --text-subtle: #94a3b8;
--text-subtle: #a1a1aa; --border-color: #e2e8f0;
--border-color: #e4e4e7; --border-subtle: #f1f5f9;
--border-hover: #a1a1aa; --border-hover: #cbd5e1;
--primary: #111113; --primary: #0f172a;
--primary-fg: #ffffff; --primary-fg: #ffffff;
--bar-fill: #18181b; --accent: #2563eb;
--dot-online: #18181b; --accent-soft: rgba(37, 99, 235, 0.08);
--card-shadow: 0 2px 10px -2px rgba(0, 0, 0, 0.04); --success: #10b981;
--success-soft: rgba(16, 185, 129, 0.10);
--card-shadow: 0 1px 3px 0 rgba(15, 23, 42, 0.04), 0 1px 2px -1px rgba(15, 23, 42, 0.03);
}
[data-theme="catppuccin"] {
--bg-page: #11111b;
--bg-card: #1e1e2e;
--bg-widget: #181825;
--bg-muted: #313244;
--bg-hover: #242438;
--text-main: #cdd6f4;
--text-muted: #a6adc8;
--text-subtle: #6c7086;
--border-color: #313244;
--border-subtle: rgba(205, 214, 244, 0.06);
--border-hover: #45475a;
--primary: #cba6f7;
--primary-fg: #11111b;
--accent: #89b4fa;
--accent-soft: rgba(137, 180, 250, 0.15);
--success: #a6e3a1;
--success-soft: rgba(166, 227, 161, 0.15);
--card-shadow: 0 4px 16px -4px rgba(17, 17, 27, 0.6);
}
[data-theme="nord"] {
--bg-page: #242933;
--bg-card: #2e3440;
--bg-widget: #292e39;
--bg-muted: #3b4252;
--bg-hover: #353c4a;
--text-main: #eceff4;
--text-muted: #d8dee9;
--text-subtle: #81a1c1;
--border-color: #3b4252;
--border-subtle: rgba(236, 239, 244, 0.06);
--border-hover: #4c566a;
--primary: #88c0d0;
--primary-fg: #2e3440;
--accent: #88c0d0;
--accent-soft: rgba(136, 192, 208, 0.15);
--success: #a3be8c;
--success-soft: rgba(163, 190, 140, 0.15);
--card-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.35);
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-page); background-color: var(--bg-page);
color: var(--text-main); color: var(--text-main);
transition: background-color 0.15s ease, color 0.15s ease; transition: background-color 0.2s ease, color 0.2s ease;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -71,8 +118,8 @@
background-color: var(--bg-card); background-color: var(--bg-card);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border-radius: 0.625rem; border-radius: 0.75rem;
transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease; transition: border-color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
} }
.hl-card-interactive:hover { .hl-card-interactive:hover {
@@ -89,7 +136,7 @@
.hl-pill { .hl-pill {
background-color: var(--bg-card); background-color: var(--bg-card);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.375rem; border-radius: 0.5rem;
} }
.hl-muted { .hl-muted {
@@ -104,18 +151,18 @@
background-color: var(--primary); background-color: var(--primary);
color: var(--primary-fg); color: var(--primary-fg);
font-weight: 500; font-weight: 500;
border-radius: 0.375rem; border-radius: 0.5rem;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
} }
.hl-btn-primary:hover { .hl-btn-primary:hover {
opacity: 0.88; opacity: 0.9;
} }
.hl-btn-outline { .hl-btn-outline {
background-color: var(--bg-card); background-color: var(--bg-card);
color: var(--text-main); color: var(--text-main);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 0.375rem; border-radius: 0.5rem;
transition: background-color 0.15s ease, border-color 0.15s ease; transition: background-color 0.15s ease, border-color 0.15s ease;
} }
.hl-btn-outline:hover { .hl-btn-outline:hover {
@@ -124,7 +171,12 @@
} }
.hl-bar-fill { .hl-bar-fill {
background-color: var(--bar-fill); background-color: var(--accent);
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.hl-bar-fill-green {
background-color: var(--success);
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1); transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
} }
@@ -134,105 +186,115 @@
<body class="min-h-screen"> <body class="min-h-screen">
<div id="app" v-cloak class="min-h-screen flex flex-col"> <div id="app" v-cloak class="min-h-screen flex flex-col">
<!-- Monochrome Toast Notification --> <!-- Toast Notification -->
<div v-if="toast" class="fixed bottom-5 right-5 z-50 hl-card px-4 py-3 flex items-center gap-2.5 shadow-2xl"> <div v-if="toast" class="fixed bottom-5 right-5 z-50 hl-card px-4 py-3 flex items-center gap-2.5 shadow-2xl">
<span class="w-1.5 h-1.5 rounded-full" style="background-color: var(--primary)"></span> <span class="w-2 h-2 rounded-full" style="background-color: var(--success)"></span>
<span class="text-xs font-mono-num">{{ toast }}</span> <span class="text-xs font-medium">{{ toast }}</span>
</div> </div>
<!-- ======================================================================= <!-- =======================================================================
1. Top Utility & System Resources Ribbon (Pure Monochrome Geist Style) 1. gethomepage.dev Style Top Utility & Resources Ribbon
======================================================================= --> ======================================================================= -->
<header class="border-b hl-border sticky top-0 z-30 backdrop-blur-md" style="background-color: color-mix(in srgb, var(--bg-page) 88%, transparent);"> <header class="border-b hl-border sticky top-0 z-30 backdrop-blur-md" style="background-color: color-mix(in srgb, var(--bg-page) 86%, transparent);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between gap-3"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex flex-wrap items-center justify-between gap-3">
<!-- Left: Brand + Live System Micro-Gauges --> <!-- Left: Brand + Live System Micro-Gauges Ribbon -->
<div class="flex items-center gap-3"> <div class="flex flex-wrap items-center gap-2.5 sm:gap-3">
<div class="flex items-center gap-2.5 pr-2"> <div class="flex items-center gap-2.5 pr-1">
<div class="w-6 h-6 rounded flex items-center justify-center hl-btn-primary"> <img src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png" alt="Mac mini" class="w-6 h-6 rounded" />
<i data-lucide="terminal" class="w-3.5 h-3.5"></i> <span class="font-semibold text-sm tracking-tight">HomeLab</span>
</div> <span class="w-2 h-2 rounded-full" style="background-color: var(--success)" title="全屋 4 节点在线"></span>
<span class="font-mono-num font-semibold text-xs tracking-widest uppercase">HOMELAB // M6</span>
</div> </div>
<!-- Compact Monochrome Resource Gauges --> <!-- Compact Resource Ribbon (Homepage Top Widget Style) -->
<div class="hidden xl:flex items-center gap-2 text-[11px] font-mono-num"> <div class="hidden xl:flex items-center gap-2 text-xs font-mono-num">
<div class="hl-pill px-2.5 py-1 flex items-center gap-2"> <!-- CPU Gauge -->
<span style="color: var(--text-muted)">CPU</span> <div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="Apple M6 12核处理器负载">
<div class="w-10 h-1 rounded-full hl-muted overflow-hidden"> <span style="color: var(--text-muted)">M6 CPU</span>
<div class="h-full hl-bar-fill" :style="{ width: Math.max(6, telemetry.mini.cpu_pct) + '%' }"></div> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div class="h-full hl-bar-fill" :style="{ width: Math.max(5, telemetry.mini.cpu_pct) + '%' }"></div>
</div> </div>
<span>{{ telemetry.mini.cpu_pct }}%</span> <span class="font-medium">{{ telemetry.mini.cpu_pct }}%</span>
</div> </div>
<div class="hl-pill px-2.5 py-1 flex items-center gap-2"> <!-- RAM Gauge -->
<div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="Mac mini 24GB 统一内存">
<span style="color: var(--text-muted)">RAM</span> <span style="color: var(--text-muted)">RAM</span>
<div class="w-10 h-1 rounded-full hl-muted overflow-hidden"> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div class="h-full hl-bar-fill" :style="{ width: Math.max(6, telemetry.mini.mem_pct) + '%' }"></div> <div class="h-full hl-bar-fill" :style="{ width: Math.max(5, telemetry.mini.mem_pct) + '%' }"></div>
</div> </div>
<span>{{ telemetry.mini.mem_used_gb }}G</span> <span class="font-medium">{{ telemetry.mini.mem_used_gb }}G</span>
</div> </div>
<div class="hl-pill px-2.5 py-1 flex items-center gap-2"> <!-- ZSpace 20TB Gauge -->
<span style="color: var(--text-muted)">Z4 NAS</span> <div class="hl-pill px-2.5 py-1 flex items-center gap-2" title="极空间 Z4 20TB 存储阵列">
<div class="w-10 h-1 rounded-full hl-muted overflow-hidden"> <span style="color: var(--text-muted)">NAS 余量</span>
<div class="h-full hl-bar-fill" :style="{ width: Math.max(6, telemetry.nas.pct) + '%' }"></div> <div class="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div class="h-full hl-bar-fill-green" :style="{ width: Math.max(5, telemetry.nas.pct) + '%' }"></div>
</div> </div>
<span>{{ telemetry.nas.free_tb }}T FREE</span> <span class="font-medium" style="color: var(--success)">{{ telemetry.nas.free_tb }} TB</span>
</div> </div>
<div class="hl-pill px-2.5 py-1 flex items-center gap-1.5"> <!-- SG AI Line Gauge -->
<span style="color: var(--text-muted)">SG EXIT</span> <div class="hl-pill px-2.5 py-1 flex items-center gap-1.5" title="AGY 锁定新加坡 Azure 专线出口">
<span>{{ telemetry.network.sg_delay_ms > 0 ? telemetry.network.sg_delay_ms + 'ms' : 'LOCKED' }}</span> <span class="w-1.5 h-1.5 rounded-full" style="background-color: var(--success)"></span>
<span style="color: var(--text-muted)">SG 专线</span>
<span class="font-medium">{{ telemetry.network.sg_delay_ms > 0 ? telemetry.network.sg_delay_ms + 'ms' : '直连' }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Right: Cmd+K, Route Switcher, Dark/Light Toggle, Terminal Trigger --> <!-- Right: Search (Cmd+K), Route Switcher, Theme Switcher, Terminal Button -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Cmd+K Trigger --> <!-- Cmd+K Command Palette Trigger -->
<button <button
@click="openCmdPalette" @click="openCmdPalette"
class="hl-btn-outline px-2.5 py-1 text-xs flex items-center gap-3" class="hl-btn-outline px-2.5 py-1 text-xs flex items-center gap-3"
title="按 Cmd+K 或 / 键唤起全局命令面板"
> >
<span class="flex items-center gap-1.5" style="color: var(--text-muted)"> <span class="flex items-center gap-1.5" style="color: var(--text-muted)">
<i data-lucide="search" class="w-3.5 h-3.5"></i> <i data-lucide="search" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Search...</span> <span class="hidden sm:inline">搜索或执行指令...</span>
</span> </span>
<kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">⌘K</kbd> <kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">⌘K</kbd>
</button> </button>
<!-- Route Mode Segmented Switcher --> <!-- Route Mode Segmented Switcher -->
<div class="hidden md:flex items-center p-0.5 rounded-md hl-muted border hl-border text-[11px] font-mono-num"> <div class="hidden md:flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
<button <button
v-for="m in modes" v-for="m in modes"
:key="m.id" :key="m.id"
@click="routeMode = m.id" @click="routeMode = m.id"
class="px-2.5 py-1 rounded transition-all" class="px-2 py-1 rounded-md font-medium transition-all"
:style="routeMode === m.id ? 'background-color: var(--bg-card); color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.12);' : 'color: var(--text-muted);'" :style="routeMode === m.id ? 'background-color: var(--bg-card); color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.08);' : 'color: var(--text-muted);'"
> >
{{ m.label }} {{ m.label }}
</button> </button>
</div> </div>
<!-- Pure Dark / Light Toggle --> <!-- Theme Selector -->
<div class="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
<button <button
@click="toggleTheme" v-for="t in themes"
class="hl-btn-outline p-1.5 text-xs flex items-center justify-center" :key="t.id"
:title="currentTheme === 'dark' ? '切换为浅色钛银模式' : '切换为深色黑曜石模式'" @click="setTheme(t.id)"
class="px-2 py-1 rounded-md font-medium transition-all flex items-center gap-1.5"
:style="currentTheme === t.id ? 'background-color: var(--bg-card); color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.08);' : 'color: var(--text-muted);'"
:title="t.label"
> >
<i v-if="currentTheme === 'dark'" data-lucide="sun" class="w-3.5 h-3.5"></i> <span class="w-2 h-2 rounded-full border border-black/10" :style="{ backgroundColor: t.dot }"></span>
<i v-else data-lucide="moon" class="w-3.5 h-3.5"></i> <span class="hidden lg:inline">{{ t.label }}</span>
</button> </button>
</div>
<!-- Embedded AGY Terminal Trigger --> <!-- Embedded AGY Terminal Trigger -->
<button <button
@click="showAgyModal = true" @click="showAgyModal = true"
class="hl-btn-primary px-3 py-1.5 text-xs font-mono-num flex items-center gap-1.5" class="hl-btn-primary px-3 py-1.5 text-xs flex items-center gap-1.5"
> >
<i data-lucide="terminal-square" class="w-3.5 h-3.5"></i> <i data-lucide="terminal" class="w-3.5 h-3.5"></i>
<span>AGY CLI</span> <span>AGY 终端</span>
</button> </button>
</div> </div>
@@ -240,22 +302,23 @@
</header> </header>
<!-- ======================================================================= <!-- =======================================================================
2. Main Categorized 3-Column Rows (Homepage Layout + Monochrome Aesthetics) 2. Main Categorized 3-Column Rows (gethomepage.dev Signature Layout)
Each card embeds a 3-cell Live Telemetry Widget Block at its footer!
======================================================================= --> ======================================================================= -->
<main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-7 space-y-8"> <main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
<!-- Category Group Loop --> <!-- Category Group Loop -->
<section v-for="group in serviceGroups" :key="group.title" class="space-y-3.5"> <section v-for="group in serviceGroups" :key="group.title" class="space-y-3">
<!-- Group Header --> <!-- Group Header -->
<div class="flex items-center justify-between px-0.5 border-b hl-border pb-2"> <div class="flex items-center justify-between px-0.5">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2">
<h2 class="text-xs font-mono-num font-semibold uppercase tracking-widest"> <h2 class="text-xs font-semibold uppercase tracking-wider" style="color: var(--text-muted)">
{{ group.title }} {{ group.title }}
</h2> </h2>
<span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">// {{ group.subtitle }}</span> <span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">· {{ group.subtitle }}</span>
</div> </div>
<span v-if="group.showRouteHint" class="text-[11px] font-mono-num hidden sm:inline" style="color: var(--text-muted)"> <span v-if="group.showRouteHint" class="text-[11px] font-mono-num hidden sm:inline" style="color: var(--text-subtle)">
MODE: {{ activeModeLabel }} 当前路由模式: {{ activeModeLabel }}
</span> </span>
</div> </div>
@@ -269,20 +332,19 @@
rel="noopener noreferrer" rel="noopener noreferrer"
class="hl-card hl-card-interactive flex flex-col justify-between overflow-hidden group cursor-pointer" class="hl-card hl-card-interactive flex flex-col justify-between overflow-hidden group cursor-pointer"
> >
<!-- Card Top --> <!-- Card Top: Authentic Brand Icon + Title + Status Dot + Description -->
<div class="p-5 space-y-3"> <div class="p-4 sm:p-5 space-y-3">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3.5 min-w-0">
<!-- Crisp Monochrome Vector Icon Box --> <div class="w-10 h-10 rounded-xl hl-muted border hl-border p-1.5 flex items-center justify-center shrink-0">
<div class="w-9 h-9 rounded-lg hl-muted border hl-border flex items-center justify-center shrink-0"> <img :src="card.iconUrl" :alt="card.title" class="w-full h-full object-contain" />
<i :data-lucide="card.icon" class="w-4 h-4"></i>
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="text-sm font-semibold tracking-tight truncate">{{ card.title }}</h3> <h3 class="text-sm font-semibold truncate group-hover:underline">{{ card.title }}</h3>
<span <span
class="w-1.5 h-1.5 rounded-full shrink-0" class="w-2 h-2 rounded-full shrink-0"
:style="{ backgroundColor: card.online !== false ? 'var(--dot-online)' : '#71717a' }" :style="{ backgroundColor: card.online !== false ? 'var(--success)' : '#ef4444' }"
></span> ></span>
</div> </div>
<p class="text-[11px] font-mono-num truncate mt-0.5" style="color: var(--text-muted)"> <p class="text-[11px] font-mono-num truncate mt-0.5" style="color: var(--text-muted)">
@@ -291,23 +353,25 @@
</div> </div>
</div> </div>
<!-- Action Pill or Route Tag --> <!-- Action Pill or External Arrow -->
<button <button
v-if="card.actionType === 'drawer'" v-if="card.actionType === 'drawer'"
@click.prevent="showAgyModal = true" @click.prevent="showAgyModal = true"
class="hl-btn-primary px-2.5 py-1 text-[11px] font-mono-num shrink-0" class="px-2 py-1 rounded-md text-[11px] font-medium shrink-0 transition-opacity hover:opacity-80"
style="background-color: var(--accent-soft); color: var(--accent);"
> >
页内终端 页内终端
</button> </button>
<button <button
v-else-if="card.actionType === 'backup'" v-else-if="card.actionType === 'backup'"
@click.prevent="triggerAction('backup_nas', '正在打包备份至极空间 Z4...')" @click.prevent="triggerAction('backup_nas', '正在打包备份至极空间 Z4...')"
class="hl-btn-outline px-2.5 py-1 text-[11px] font-mono-num shrink-0" class="px-2 py-1 rounded-md text-[11px] font-medium shrink-0 transition-opacity hover:opacity-80"
style="background-color: var(--success-soft); color: var(--success);"
> >
立即备份 立即备份
</button> </button>
<span v-else class="text-[11px] font-mono-num px-2 py-0.5 rounded hl-muted shrink-0" style="color: var(--text-muted)"> <span v-else class="text-xs font-mono-num px-2 py-0.5 rounded hl-muted shrink-0" style="color: var(--text-muted)">
{{ resolveBadge(card) }} ↗ {{ resolveBadge(card) }}
</span> </span>
</div> </div>
@@ -316,17 +380,20 @@
</p> </p>
</div> </div>
<!-- Card Footer: 3-Cell Inline Telemetry Widget (100% Monochrome Tabular Numbers) --> <!-- Card Footer: gethomepage.dev Signature 3-Cell Inline Telemetry Widget -->
<div class="hl-widget-footer grid grid-cols-3 divide-x hl-border text-center"> <div class="hl-widget-footer grid grid-cols-3 divide-x hl-border text-center">
<div <div
v-for="(stat, idx) in card.stats" v-for="(stat, idx) in card.stats"
:key="idx" :key="idx"
class="py-2.5 px-2 flex flex-col items-center justify-center" class="py-2.5 px-2 flex flex-col items-center justify-center"
> >
<span class="text-[10px] font-mono-num uppercase tracking-wider truncate max-w-full" style="color: var(--text-subtle)"> <span class="text-[10px] uppercase tracking-wider truncate max-w-full" style="color: var(--text-subtle)">
{{ stat.label }} {{ stat.label }}
</span> </span>
<span class="text-xs font-semibold font-mono-num mt-0.5 truncate max-w-full"> <span
class="text-xs font-semibold font-mono-num mt-0.5 truncate max-w-full"
:style="stat.highlight ? 'color: var(--success)' : 'color: var(--text-main)'"
>
{{ stat.value }} {{ stat.value }}
</span> </span>
</div> </div>
@@ -336,30 +403,30 @@
</section> </section>
<!-- ===================================================================== <!-- =====================================================================
3. Bottom Infrastructure & Quick SSH Shelf 3. Bottom Infrastructure & Quick Bookmarks / SSH Shelf
===================================================================== --> ===================================================================== -->
<section class="space-y-3.5 pt-1"> <section class="space-y-3 pt-1">
<div class="flex items-center justify-between px-0.5 border-b hl-border pb-2"> <div class="flex items-center justify-between px-0.5">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2">
<h2 class="text-xs font-mono-num font-semibold uppercase tracking-widest"> <h2 class="text-xs font-semibold uppercase tracking-wider" style="color: var(--text-muted)">
SSH 终端快捷指令与运维操作 终端快捷指令与备用直连书签
</h2> </h2>
<span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">// CLICK TO COPY</span> <span class="text-[11px] font-mono-num" style="color: var(--text-subtle)">· 点击指令即复制</span>
</div> </div>
<div class="flex items-center gap-4 text-xs font-mono-num"> <div class="flex items-center gap-3 text-xs">
<button <button
@click="triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...')" @click="triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...')"
class="hover:underline" class="hover:underline font-medium"
style="color: var(--text-muted)" style="color: var(--accent)"
> >
[刷新 DDNS] 刷新 DDNS 解析
</button> </button>
<button <button
@click="triggerAction('mount_nas', '正在巡检极空间 20TB SMB 挂载...')" @click="triggerAction('mount_nas', '正在巡检极空间 20TB SMB 挂载...')"
class="hover:underline" class="hover:underline font-medium"
style="color: var(--text-muted)" style="color: var(--accent)"
> >
[巡检 NAS 挂载] 巡检极空间挂载
</button> </button>
</div> </div>
</div> </div>
@@ -372,10 +439,12 @@
class="hl-card hl-card-interactive px-4 py-3 text-left flex items-center justify-between gap-2" class="hl-card hl-card-interactive px-4 py-3 text-left flex items-center justify-between gap-2"
> >
<div class="min-w-0"> <div class="min-w-0">
<div class="text-xs font-medium" style="color: var(--text-muted)">{{ item.label }}</div> <div class="text-xs font-semibold">{{ item.label }}</div>
<div class="text-xs font-mono-num font-semibold truncate mt-0.5">{{ item.cmd }}</div> <div class="text-[11px] font-mono-num truncate mt-0.5" style="color: var(--accent)">{{ item.cmd }}</div>
</div> </div>
<i data-lucide="copy" class="w-3.5 h-3.5 shrink-0" style="color: var(--text-subtle)"></i> <span class="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted shrink-0" style="color: var(--text-muted)">
COPY
</span>
</button> </button>
</div> </div>
</section> </section>
@@ -383,12 +452,12 @@
</main> </main>
<!-- ======================================================================= <!-- =======================================================================
4. Global Cmd+K Command Palette 4. Global Cmd+K Command Palette (Raycast / Shadcn Command Dialog)
======================================================================= --> ======================================================================= -->
<div <div
v-if="showCmdPalette" v-if="showCmdPalette"
@click.self="showCmdPalette = false" @click.self="showCmdPalette = false"
class="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-start justify-center pt-20 px-4" class="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-start justify-center pt-20 px-4"
> >
<div class="w-full max-w-xl hl-card overflow-hidden shadow-2xl"> <div class="w-full max-w-xl hl-card overflow-hidden shadow-2xl">
<div class="px-4 py-3 border-b hl-border flex items-center gap-3"> <div class="px-4 py-3 border-b hl-border flex items-center gap-3">
@@ -397,7 +466,7 @@
ref="cmdInputRef" ref="cmdInputRef"
v-model="cmdQuery" v-model="cmdQuery"
type="text" type="text"
placeholder="输入服务名称、SSH 指令或操作..." placeholder="输入服务名称、SSH 指令、路由模式或主题..."
class="w-full bg-transparent text-sm outline-none" class="w-full bg-transparent text-sm outline-none"
@keydown.esc="showCmdPalette = false" @keydown.esc="showCmdPalette = false"
@keydown.enter="runFirstCommand" @keydown.enter="runFirstCommand"
@@ -405,16 +474,21 @@
<kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">ESC</kbd> <kbd class="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted" style="color: var(--text-muted)">ESC</kbd>
</div> </div>
<div class="max-h-80 overflow-y-auto p-2 text-xs"> <div class="max-h-80 overflow-y-auto p-2 divide-y hl-border text-xs">
<div class="py-1 space-y-1">
<div class="px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider" style="color: var(--text-subtle)">
快速操作与服务直达
</div>
<button <button
v-for="cmd in filteredCommands" v-for="cmd in filteredCommands"
:key="cmd.id" :key="cmd.id"
@click="executeCommand(cmd)" @click="executeCommand(cmd)"
class="w-full px-3 py-2.5 rounded-md text-left flex items-center justify-between transition-colors" class="w-full px-3 py-2 rounded-lg text-left flex items-center justify-between hover:opacity-90 transition-colors"
style="background-color: transparent;"
onmouseover="this.style.backgroundColor='var(--bg-muted)'" onmouseover="this.style.backgroundColor='var(--bg-muted)'"
onmouseout="this.style.backgroundColor='transparent'" onmouseout="this.style.backgroundColor='transparent'"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-2.5">
<span class="font-medium">{{ cmd.title }}</span> <span class="font-medium">{{ cmd.title }}</span>
<span class="font-mono-num text-[11px]" style="color: var(--text-muted)">{{ cmd.hint }}</span> <span class="font-mono-num text-[11px]" style="color: var(--text-muted)">{{ cmd.hint }}</span>
</div> </div>
@@ -425,23 +499,24 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ======================================================================= <!-- =======================================================================
5. Embedded AGY Cloud Terminal Modal 5. Embedded AGY Cloud Terminal Modal (tmux persistent workbench)
======================================================================= --> ======================================================================= -->
<div v-if="showAgyModal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"> <div v-if="showAgyModal" class="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6">
<div class="w-full h-full max-w-6xl hl-card flex flex-col overflow-hidden shadow-2xl"> <div class="w-full h-full max-w-6xl hl-card flex flex-col overflow-hidden shadow-2xl">
<div class="px-4 py-2.5 border-b hl-border flex items-center justify-between hl-muted"> <div class="px-4 py-2.5 border-b hl-border flex items-center justify-between hl-muted">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<i data-lucide="terminal" class="w-4 h-4"></i> <img src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/iterm2.png" class="w-4 h-4" />
<span class="text-xs font-mono-num font-medium">AGY CLOUD WORKBENCH // tmux attach -t agy</span> <span class="text-xs font-mono-num font-medium">AGY 云端工作台 · tmux 持久会话 (关窗不断连)</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a :href="agyModalUrl" target="_blank" class="hl-btn-outline px-2.5 py-1 text-xs font-mono-num"> <a :href="agyModalUrl" target="_blank" class="hl-btn-outline px-2.5 py-1 text-xs font-mono-num">
FULLSCREEN ↗ 新标签页全屏 ↗
</a> </a>
<button @click="showAgyModal = false" class="hl-btn-primary px-3 py-1 text-xs font-mono-num"> <button @click="showAgyModal = false" class="hl-btn-primary px-3 py-1 text-xs">
CLOSE 收起终端
</button> </button>
</div> </div>
</div> </div>
@@ -456,8 +531,7 @@
createApp({ createApp({
setup() { setup() {
const storedTheme = localStorage.getItem('hl_mono_theme'); const currentTheme = ref(localStorage.getItem('hl_theme') || 'zinc-dark');
const currentTheme = ref(storedTheme === 'light' ? 'light' : 'dark');
const routeMode = ref(localStorage.getItem('hl_route_mode') || 'auto'); const routeMode = ref(localStorage.getItem('hl_route_mode') || 'auto');
const showAgyModal = ref(false); const showAgyModal = ref(false);
const showCmdPalette = ref(false); const showCmdPalette = ref(false);
@@ -466,24 +540,24 @@
const toast = ref(''); const toast = ref('');
const actionLoading = ref(false); const actionLoading = ref(false);
const modes = [ const themes = [
{ id: 'auto', label: 'AUTO' }, { id: 'zinc-dark', label: '深色极客', dot: '#18181b' },
{ id: 'edge443', label: '443 免端口' }, { id: 'zinc-light', label: '浅色纯净', dot: '#f8fafc' },
{ id: 'home10443', label: '10443 家宽' }, { id: 'catppuccin', label: 'Catppuccin', dot: '#cba6f7' },
{ id: 'lan', label: 'LAN 内网' }, { id: 'nord', label: 'Nord', dot: '#88c0d0' },
]; ];
const applyTheme = (id) => { const modes = [
currentTheme.value = id; { id: 'auto', label: '智能路由' },
localStorage.setItem('hl_mono_theme', id); { id: 'edge443', label: '443 免端口' },
document.documentElement.setAttribute('data-theme', id); { id: 'home10443', label: '10443 家宽' },
nextTick(() => { { id: 'lan', label: 'LAN 局域网' },
if (window.lucide) window.lucide.createIcons(); ];
});
};
const toggleTheme = () => { const setTheme = (id) => {
applyTheme(currentTheme.value === 'dark' ? 'light' : 'dark'); currentTheme.value = id;
localStorage.setItem('hl_theme', id);
document.documentElement.setAttribute('data-theme', id);
}; };
const telemetry = ref({ const telemetry = ref({
@@ -500,6 +574,7 @@
disk_total_gb: 460.4, disk_total_gb: 460.4,
disk_pct: 13.0, disk_pct: 13.0,
uptime: '运行中', uptime: '运行中',
containers: [{ name: 'vaultwarden' }],
agy_tmux: true, agy_tmux: true,
}, },
nas: { nas: {
@@ -544,122 +619,123 @@
const activeModeLabel = computed(() => { const activeModeLabel = computed(() => {
const m = effectiveMode.value; const m = effectiveMode.value;
if (m === 'lan') return 'LAN 192.168.31.x'; if (m === 'lan') return 'LAN 局域网极速直连 (192.168.31.x)';
if (m === 'home10443') return 'WAN 10443 DIRECT'; if (m === 'home10443') return '10443 家宽公网 HTTPS 直连';
return 'EDGE 443 HTTPS'; return '443 免端口 HTTPS 直达 (*.961504.xyz)';
}); });
// Categorized Service Rows with Homepage-style Inline Telemetry Widgets
const serviceGroups = computed(() => { const serviceGroups = computed(() => {
const t = telemetry.value; const t = telemetry.value;
return [ return [
{ {
title: '01 / 核心工作台与私有云存储', title: '核心工作台与私有云存储',
subtitle: 'WORKBENCH & STORAGE', subtitle: 'Core Workbench & Storage',
showRouteHint: true, showRouteHint: true,
items: [ items: [
{ {
id: 'agy', id: 'agy',
title: 'AGY 云端工作台', title: 'AGY 云端工作台',
subtitle: 'agy.961504.xyz · ttyd + tmux', subtitle: 'agy.961504.xyz · ttyd + tmux',
icon: 'terminal', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/iterm2.png',
online: t.services.agy, online: t.services.agy,
actionType: 'drawer', actionType: 'drawer',
desc: 'M6 Mac mini 上的 24 小时常驻 AI 编程终端,已同步登录态并锁定新加坡出口,关窗后台继续运行。', desc: 'M6 Mac mini 上的 24 小时常驻 AI 编程终端,已同步 Google 登录态并锁定新加坡出口,关掉网页后台继续运行。',
edgeUrl: 'https://agy.961504.xyz', edgeUrl: 'https://agy.961504.xyz',
homeUrl: 'https://agy.home.961504.xyz:10443', homeUrl: 'https://agy.home.961504.xyz:10443',
lanUrl: 'http://192.168.31.164:7681', lanUrl: 'http://192.168.31.164:7681',
stats: [ stats: [
{ label: 'M6 CPU', value: `${t.mini.cpu_pct}%` }, { label: 'M6 CPU 负载', value: `${t.mini.cpu_pct}%` },
{ label: 'MEMORY', value: `${t.mini.mem_used_gb} / 24G` }, { label: '已用内存', value: `${t.mini.mem_used_gb} / 24 GB` },
{ label: 'TMUX', value: t.mini.agy_tmux ? 'ACTIVE' : 'IDLE' }, { label: 'tmux 会话', value: t.mini.agy_tmux ? '常驻运行中' : '待机', highlight: true },
] ]
}, },
{ {
id: 'zspace', id: 'zspace',
title: '极空间 Z4 私有云', title: '极空间 Z4 私有云',
subtitle: '192.168.31.185 · 20TB RAID', subtitle: '192.168.31.185 · 20TB RAID 存储池',
icon: 'hard-drive', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/synology-drive.png',
online: t.services.nas, online: t.services.nas,
desc: '家庭 20TB 大容量存储中心、极影视蓝光影院与手机相册备份,已自动挂载至 Mac mini ~/nas。', desc: '家庭 20TB 大容量存储中心、极影视蓝光影院与手机相册备份,已自动 SMB 挂载至 Mac mini ~/nas。',
edgeUrl: 'https://z4.home.961504.xyz:10443', edgeUrl: 'https://z4.home.961504.xyz:10443',
homeUrl: 'https://z4.home.961504.xyz:10443', homeUrl: 'https://z4.home.961504.xyz:10443',
lanUrl: 'http://192.168.31.185:5055', lanUrl: 'http://192.168.31.185:5055',
stats: [ stats: [
{ label: 'FREE', value: `${t.nas.free_tb} TB` }, { label: '剩余可用', value: `${t.nas.free_tb} TB`, highlight: true },
{ label: 'USED', value: `${t.nas.used_tb} TB (${t.nas.pct}%)` }, { label: '已用容量', value: `${t.nas.used_tb} TB (${t.nas.pct}%)` },
{ label: 'MOUNT', value: t.nas.mounted ? '~/nas OK' : 'RETRY' }, { label: 'SMB 挂载', value: t.nas.mounted ? '~/nas 已挂载' : '重连中' },
] ]
}, },
{ {
id: 'vaultwarden', id: 'vaultwarden',
title: 'Vaultwarden 密码库', title: 'Vaultwarden 密码保险箱',
subtitle: 'vw.961504.xyz · Bitwarden', subtitle: 'vw.961504.xyz · Bitwarden 兼容',
icon: 'key-round', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/vaultwarden.png',
online: t.services.vaultwarden, online: t.services.vaultwarden,
actionType: 'backup', actionType: 'backup',
desc: '自托管全平台加密密码保险箱,支持手机与浏览器自动填充,每天凌晨 03:30 自动打包灾备至极空间。', desc: '自托管全平台加密密码库,支持 iOS / Android / 浏览器插件自动填充,每天凌晨 03:30 自动打包灾备至极空间。',
edgeUrl: 'https://vw.961504.xyz', edgeUrl: 'https://vw.961504.xyz',
homeUrl: 'https://vw.home.961504.xyz:10443', homeUrl: 'https://vw.home.961504.xyz:10443',
lanUrl: 'http://192.168.31.164:8222', lanUrl: 'http://192.168.31.164:8222',
stats: [ stats: [
{ label: 'STATUS', value: 'HEALTHY' }, { label: '容器状态', value: 'Healthy', highlight: true },
{ label: 'LAST BACKUP', value: t.nas.latest_backup }, { label: '最近极空间灾备', value: t.nas.latest_backup },
{ label: 'MAC SSD', value: `${t.mini.disk_pct}%` }, { label: 'Mac SSD 占用', value: `${t.mini.disk_pct}%` },
] ]
}, },
] ]
}, },
{ {
title: '02 / 网络网关、AI 专线与外部哨兵', title: '网络网关、AI 专线与外部哨兵',
subtitle: 'NETWORK & CLOUD EDGE', subtitle: 'Network Gateway & Cloud Edge',
showRouteHint: false, showRouteHint: false,
items: [ items: [
{ {
id: 'clash', id: 'clash',
title: 'Mihomo 路由分流面板', title: 'Mihomo 路由分流面板',
subtitle: '192.168.31.1:9999 · ShellCrash', subtitle: '192.168.31.1:9999 · ShellCrash',
icon: 'waypoints', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/clash.png',
online: t.services.clash, online: t.services.clash,
desc: '小米主路由器透明网关控制台,实时监控全屋设备流量、分流规则与狮城 AI 算力专线锁定状态。', desc: '小米主路由器透明网关控制台,实时监控全屋设备流量、分流规则与狮城 AI 算力专线锁定状态。',
edgeUrl: 'https://clash.home.961504.xyz:10443/ui/', edgeUrl: 'https://clash.home.961504.xyz:10443/ui/',
homeUrl: 'https://clash.home.961504.xyz:10443/ui/', homeUrl: 'https://clash.home.961504.xyz:10443/ui/',
lanUrl: 'http://192.168.31.1:9999/ui/', lanUrl: 'http://192.168.31.1:9999/ui/',
stats: [ stats: [
{ label: 'AI ROUTE', value: 'SG LOCKED' }, { label: 'AI 锁定策略', value: '狮城节点', highlight: true },
{ label: 'LATENCY', value: t.network.sg_delay_ms > 0 ? `${t.network.sg_delay_ms} ms` : 'DIRECT' }, { label: '新加坡延迟', value: t.network.sg_delay_ms > 0 ? `${t.network.sg_delay_ms} ms` : '直连' },
{ label: 'EGRESS IP', value: t.network.sg_egress_ip }, { label: '固定出口 IP', value: t.network.sg_egress_ip },
] ]
}, },
{ {
id: 'router', id: 'router',
title: '小米主路由器后台', title: '小米主路由器后台',
subtitle: '192.168.31.1 · PPPoE & DNAT', subtitle: '192.168.31.1 · PPPoE 公网网关',
icon: 'router', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/openwrt.png',
online: t.services.router, online: t.services.router,
desc: '管理家庭联通公网宽带拨号、DHCP 静态 IP 绑定、10443 与 41641 端口转发及全屋内网 DNS。', desc: '管理家庭联通公网宽带拨号、DHCP 静态 IP 绑定、10443 与 41641 端口转发及全屋内网 Split-DNS。',
edgeUrl: 'https://router.home.961504.xyz:10443', edgeUrl: 'https://router.home.961504.xyz:10443',
homeUrl: 'https://router.home.961504.xyz:10443', homeUrl: 'https://router.home.961504.xyz:10443',
lanUrl: 'http://192.168.31.1', lanUrl: 'http://192.168.31.1',
stats: [ stats: [
{ label: 'WAN IPv4', value: t.network.home_wan_ip }, { label: '联通公网 IPv4', value: t.network.home_wan_ip, highlight: true },
{ label: 'DDNS', value: '*.home.961504.xyz' }, { label: '泛域名 DDNS', value: '*.home.961504.xyz' },
{ label: 'GATEWAY', value: '192.168.31.1' }, { label: '内网网关', value: '192.168.31.1' },
] ]
}, },
{ {
id: 'uptime', id: 'uptime',
title: 'Uptime Kuma 外部哨兵', title: 'Uptime Kuma 外部哨兵',
subtitle: 'status.961504.xyz · SG Azure', subtitle: 'status.961504.xyz · SG Azure',
icon: 'activity', iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png',
online: t.services.uptime, online: t.services.uptime,
desc: '独立部署在新加坡 Azure 公有云上的 24 小时外部监控哨兵,通过 Tailscale P2P 专线实时探测家庭服务。', desc: '独立部署在新加坡 Azure 公有云上的 24 小时外部监控哨兵,通过 Tailscale P2P 专线实时探测家庭服务。',
edgeUrl: 'https://status.961504.xyz', edgeUrl: 'https://status.961504.xyz',
homeUrl: 'https://status.961504.xyz', homeUrl: 'https://status.961504.xyz',
lanUrl: 'https://status.961504.xyz', lanUrl: 'https://status.961504.xyz',
stats: [ stats: [
{ label: 'AZURE IP', value: '104.43.105.243' }, { label: '云端公网 IP', value: '104.43.105.243' },
{ label: 'TAILSCALE', value: '100.115.204.51' }, { label: 'Tailscale 专线', value: '100.115.204.51' },
{ label: 'UPTIME', value: '100% ONLINE' }, { label: '外部探测状态', value: '100% 在线', highlight: true },
] ]
}, },
] ]
@@ -668,10 +744,10 @@
}); });
const sshList = [ const sshList = [
{ label: 'Mac mini 主机', cmd: 'ssh mini' }, { label: 'SSH 连接 Mac mini', cmd: 'ssh mini' },
{ label: '接力 AGY 会话', cmd: 'ssh mini -t "tmux attach -t agy"' }, { label: '终端接力 AGY 会话', cmd: 'ssh mini -t "tmux attach -t agy"' },
{ label: '新加坡 Azure', cmd: 'ssh azure' }, { label: 'SSH 连接新加坡 Azure', cmd: 'ssh azure' },
{ label: '小米主路由器', cmd: 'ssh root@192.168.31.1' }, { label: 'SSH 连接小米主路由', cmd: 'ssh root@192.168.31.1' },
]; ];
const resolveUrl = (card) => { const resolveUrl = (card) => {
@@ -683,12 +759,15 @@
const resolveBadge = (card) => { const resolveBadge = (card) => {
const m = effectiveMode.value; const m = effectiveMode.value;
if (m === 'lan') return 'LAN'; if (m === 'lan') return 'LAN 直连';
if (m === 'home10443') return '10443'; if (m === 'home10443') return '10443';
return '443'; return '443 免端口';
}; };
const agyModalUrl = computed(() => resolveUrl(serviceGroups.value[0].items[0])); const agyModalUrl = computed(() => {
const card = serviceGroups.value[0].items[0];
return resolveUrl(card);
});
const showToast = (msg) => { const showToast = (msg) => {
toast.value = msg; toast.value = msg;
@@ -700,7 +779,7 @@
const copyCmd = async (cmd, label) => { const copyCmd = async (cmd, label) => {
try { try {
await navigator.clipboard.writeText(cmd); await navigator.clipboard.writeText(cmd);
showToast(`COPIED [${label}]: ${cmd}`); showToast(`已复制「${label}」: ${cmd}`);
} catch { } catch {
showToast(cmd); showToast(cmd);
} }
@@ -721,26 +800,29 @@
try { try {
const res = await fetch(`/api/action/${action}`, { method: 'POST' }); const res = await fetch(`/api/action/${action}`, { method: 'POST' });
const data = await res.json(); const data = await res.json();
showToast(data.message || 'DONE'); showToast(data.message || '操作已完成');
await fetchTelemetry(); await fetchTelemetry();
} catch { } catch {
showToast('ERROR'); showToast('请求失败');
} finally { } finally {
actionLoading.value = false; actionLoading.value = false;
} }
}; };
const allCommands = computed(() => [ const allCommands = computed(() => [
{ id: 'open-agy-drawer', title: '在页内唤起 AGY 云端终端', hint: 'tmux attach -t agy', type: 'TERM', run: () => { showAgyModal.value = true; } }, { id: 'open-agy-drawer', title: '在页内唤起 AGY 云端终端', hint: 'tmux attach -t agy', type: '终端', run: () => { showAgyModal.value = true; } },
{ id: 'open-agy', title: '新窗口打开 AGY 云端工作台', hint: 'https://agy.961504.xyz', type: 'OPEN', run: () => window.open(resolveUrl(serviceGroups.value[0].items[0]), '_blank') }, { id: 'open-agy', title: '新窗口打开 AGY 云端工作台', hint: 'https://agy.961504.xyz', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[0]), '_blank') },
{ id: 'open-z4', title: '打开 极空间 Z4 私有云 (20TB)', hint: '192.168.31.185:5055', type: 'OPEN', run: () => window.open(resolveUrl(serviceGroups.value[0].items[1]), '_blank') }, { id: 'open-z4', title: '打开 极空间 Z4 私有云 (20TB)', hint: '192.168.31.185:5055', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[1]), '_blank') },
{ id: 'open-vw', title: '打开 Vaultwarden 密码库', hint: 'https://vw.961504.xyz', type: 'OPEN', run: () => window.open(resolveUrl(serviceGroups.value[0].items[2]), '_blank') }, { id: 'open-vw', title: '打开 Vaultwarden 密码保险箱', hint: 'https://vw.961504.xyz', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[0].items[2]), '_blank') },
{ id: 'open-clash', title: '打开 Mihomo 路由分流面板', hint: '192.168.31.1:9999', type: 'OPEN', run: () => window.open(resolveUrl(serviceGroups.value[1].items[0]), '_blank') }, { id: 'open-clash', title: '打开 Mihomo 路由分流面板', hint: '192.168.31.1:9999', type: '跳转', run: () => window.open(resolveUrl(serviceGroups.value[1].items[0]), '_blank') },
{ id: 'act-backup', title: '立即打包备份至极空间 Z4', hint: 'backup-to-nas.sh', type: 'EXEC', run: () => triggerAction('backup_nas', '正在打包备份至极空间 Z4...') }, { id: 'act-backup', title: '立即打包备份核心数据至极空间 Z4', hint: 'backup-to-nas.sh', type: '运维', run: () => triggerAction('backup_nas', '正在打包备份至极空间 Z4...') },
{ id: 'act-ddns', title: '立即刷新 Cloudflare DDNS', hint: 'cloudflare-ddns.sh', type: 'EXEC', run: () => triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...') }, { id: 'act-ddns', title: '立即刷新 Cloudflare DDNS 解析', hint: 'cloudflare-ddns.sh', type: '运维', run: () => triggerAction('refresh_ddns', '正在同步 Cloudflare DDNS...') },
{ id: 'ssh-mini', title: '复制 SSH 连接 Mac mini 指令', hint: 'ssh mini', type: 'COPY', run: () => copyCmd('ssh mini', 'Mac mini') }, { id: 'ssh-mini', title: '复制 SSH 连接 Mac mini 指令', hint: 'ssh mini', type: 'SSH', run: () => copyCmd('ssh mini', 'SSH Mac mini') },
{ id: 'ssh-agy', title: '复制 终端接力 AGY 会话指令', hint: 'ssh mini -t "tmux attach -t agy"', type: 'COPY', run: () => copyCmd('ssh mini -t "tmux attach -t agy"', 'AGY') }, { id: 'ssh-agy', title: '复制 终端接力 AGY 会话指令', hint: 'ssh mini -t "tmux attach -t agy"', type: 'SSH', run: () => copyCmd('ssh mini -t "tmux attach -t agy"', '接力 AGY') },
{ id: 'theme-toggle', title: '切换 深色黑曜石 / 浅色钛银 主题', hint: 'Dark / Light', type: 'THEME', run: () => toggleTheme() }, { id: 'theme-light', title: '切换主题: 浅色纯净 (Shadcn Light)', hint: 'zinc-light', type: '主题', run: () => setTheme('zinc-light') },
{ id: 'theme-dark', title: '切换主题: 深色极客 (Shadcn Dark)', hint: 'zinc-dark', type: '主题', run: () => setTheme('zinc-dark') },
{ id: 'theme-cat', title: '切换主题: Catppuccin Mocha', hint: 'catppuccin', type: '主题', run: () => setTheme('catppuccin') },
{ id: 'theme-nord', title: '切换主题: Nord 极地霜蓝', hint: 'nord', type: '主题', run: () => setTheme('nord') },
]); ]);
const filteredCommands = computed(() => { const filteredCommands = computed(() => {
@@ -769,7 +851,10 @@
}; };
onMounted(() => { onMounted(() => {
applyTheme(currentTheme.value); setTheme(currentTheme.value);
nextTick(() => {
if (window.lucide) window.lucide.createIcons();
});
fetchTelemetry(); fetchTelemetry();
setInterval(fetchTelemetry, 3000); setInterval(fetchTelemetry, 3000);
@@ -782,8 +867,9 @@
}); });
return { return {
themes,
currentTheme, currentTheme,
toggleTheme, setTheme,
modes, modes,
routeMode, routeMode,
activeModeLabel, activeModeLabel,