refactor(ui): merge instances page into network dashboard

- Remove the redundant Instances page; its network detail and server
  connection sections now live in DashboardView below selectable
  instance cards
- Redirect legacy /instances and /general routes to /
- Add per-page subtitles to the header instead of a fixed caption
- Widen content container to 1700px for large screens
- Highlight online device count in green on the stats cards
This commit is contained in:
lbl
2026-08-22 04:04:30 +08:00
parent b7151880c0
commit 53e092b63d
10 changed files with 323 additions and 352 deletions
+2 -2
View File
@@ -79,7 +79,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", handleKeydown));
<div class="min-w-0">
<h1 class="truncate text-lg font-bold text-slate-900 dark:text-white">{{ pageMeta.label }}</h1>
<p class="hidden text-xs text-slate-400 sm:block">VNT 虚拟局域网管理</p>
<p class="hidden text-xs text-slate-400 sm:block">{{ pageMeta.subtitle || "VNT 虚拟局域网管理" }}</p>
</div>
<div class="ml-auto flex items-center gap-2">
@@ -103,7 +103,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", handleKeydown));
</header>
<main class="custom-scrollbar min-h-0 flex-1 overflow-x-hidden overflow-y-auto">
<div class="mx-auto w-full max-w-7xl px-4 py-5 sm:px-5 lg:px-7 lg:py-6">
<div class="mx-auto w-full max-w-[1700px] px-4 py-5 sm:px-5 lg:px-7 lg:py-6">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in"><component :is="Component" /></transition>
</router-view>
+8 -8
View File
@@ -3,43 +3,43 @@ export const navItems = [
to: "/",
label: "网络总览",
shortLabel: "总览",
subtitle: "组网状态与实例管理",
icon: "M4 4h6v6H4V4Zm10 0h6v6h-6V4ZM4 14h6v6H4v-6Zm10 0h6v6h-6v-6Z",
},
{
to: "/instances",
label: "运行实例",
shortLabel: "实例",
icon: "M4 18V8m5 10V4m6 14v-7m5 7V6",
},
{
to: "/peers",
label: "在线设备",
shortLabel: "设备",
subtitle: "查看网络中的对端设备",
icon: "M15 19a6 6 0 0 0-12 0m18 0a5 5 0 0 0-5-5m-7-3a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm8 0a3 3 0 1 0 0-6",
},
{
to: "/routes",
label: "路由表",
shortLabel: "路由",
subtitle: "虚拟网络路由信息",
icon: "M6 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm12 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 6h6a4 4 0 0 1 4 4v2m-3-3 3 3 3-3M16 18h-6a4 4 0 0 1-4-4v-2",
},
{
to: "/config",
label: "组网配置",
shortLabel: "配置",
icon: "M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm7.4-.5a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3A1.7 1.7 0 0 0 14 21v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14h-.2v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z",
subtitle: "管理组网配置文件",
icon: "M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm7.4-.5a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3A1.7 1.7 0 0 0 14 21v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14h-.2v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6-1Z",
},
{
to: "/web-access",
label: "Web 访问",
shortLabel: "Web",
subtitle: "远程访问设置",
desktopOnly: true,
icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0 0c2.2-2.5 3.3-5.5 3.3-9S14.2 5.5 12 3m0 18c-2.2-2.5-3.3-5.5-3.3-9S9.8 5.5 12 3M3.5 9h17m-17 6h17",
icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0 0c-2.2-2.5 3.3-5.5 3.3-9S14.2 5.5 12 3m0 18c-2.2-2.5-3.3-5.5-3.3-9S9.8 5.5 12 3M3.5 9h17m-17 6h17",
},
{
to: "/about",
label: "关于",
shortLabel: "关于",
subtitle: "版本与更新",
icon: "M12 17v-6m0-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
},
];
+3 -4
View File
@@ -1,6 +1,5 @@
import { createRouter, createWebHashHistory } from "vue-router";
import DashboardView from "../views/DashboardView.vue";
import InstancesView from "../views/InstancesView.vue";
import ConfigView from "../views/ConfigView.vue";
import PeersView from "../views/PeersView.vue";
import RoutesView from "../views/RoutesView.vue";
@@ -9,9 +8,9 @@ import AboutView from "../views/AboutView.vue";
const routes = [
{ path: "/", component: DashboardView },
{ path: "/instances", component: InstancesView },
// 兼容旧路由
{ path: "/general", redirect: "/instances" },
// 兼容旧路由:实例管理已并入网络总览
{ path: "/instances", redirect: "/" },
{ path: "/general", redirect: "/" },
{ path: "/config", component: ConfigView },
{ path: "/peers", component: PeersView },
{ path: "/routes", component: RoutesView },
+133 -9
View File
@@ -60,7 +60,10 @@ const statusSummary = computed(() => {
</div>
<div class="card">
<div class="text-xs font-medium muted">在线设备</div>
<div class="mt-2 text-2xl font-bold tabular-nums text-slate-900 dark:text-white">
<div
class="mt-2 text-2xl font-bold tabular-nums"
:class="onlineDevices > 0 ? 'text-green-600 dark:text-green-400' : 'text-slate-900 dark:text-white'"
>
{{ onlineDevices }}
</div>
</div>
@@ -81,23 +84,144 @@ const statusSummary = computed(() => {
<!-- 启动组网 -->
<StartPanel />
<!-- 实例概览 -->
<!-- 实例列表 -->
<div>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-base font-bold text-slate-900 dark:text-white">实例概览</h2>
<router-link
to="/instances"
class="text-sm font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400"
>查看详情 </router-link
>
<h2 class="text-base font-bold text-slate-900 dark:text-white">组网实例</h2>
<span v-if="app.instanceList.length > 0" class="text-xs muted">点击卡片查看详情</span>
</div>
<EmptyState
v-if="app.instanceList.length === 0"
:text="app.configList.length === 0 ? '暂无配置,请先新建配置' : '暂无运行中的组网,请在上方选择配置启动'"
/>
<TransitionGroup v-else name="card-list" tag="div" class="grid grid-cols-1 gap-4 md:grid-cols-2">
<InstanceCard v-for="inst in app.instanceList" :key="inst.file_name" :inst="inst" />
<InstanceCard v-for="inst in app.instanceList" :key="inst.file_name" :inst="inst" selectable />
</TransitionGroup>
</div>
<!-- 选中实例详情 -->
<template v-if="app.selectedInstance && app.selectedInfo">
<div class="card">
<h2 class="mb-4 text-lg font-bold text-slate-900 dark:text-white">
网络详情
<span class="ml-2 text-sm font-medium text-indigo-600 dark:text-indigo-400">{{
app.selectedConfigName
}}</span>
</h2>
<div class="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">虚拟 IP / 掩码</span>
<span class="font-mono tabular-nums text-slate-900 dark:text-white"
>{{ app.selectedInfo.ip || "-" }} / {{ app.selectedInfo.prefix_len || "-" }}</span
>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">网关</span>
<span class="font-mono text-slate-900 dark:text-white">{{ app.selectedInfo.gateway || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">网络编号</span>
<span class="font-mono text-slate-900 dark:text-white">{{ app.selectedInfo.network_code || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">MTU</span>
<span class="font-mono tabular-nums text-slate-900 dark:text-white">{{ app.selectedInfo.mtu || "" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">NAT 类型</span>
<span class="font-mono text-indigo-600 dark:text-indigo-400">{{
app.selectedInfo.nat_type || "Unknown"
}}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">Public IPv6</span>
<span
class="max-w-[200px] truncate font-mono text-slate-900 dark:text-white"
:title="app.selectedInfo.public_ipv6"
>{{ app.selectedInfo.public_ipv6 || "-" }}</span
>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">设备名称</span>
<span class="text-slate-900 dark:text-white">{{ app.selectedInfo.name || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">设备 ID</span>
<span
class="max-w-[200px] truncate font-mono text-xs text-slate-500 dark:text-slate-400"
:title="app.selectedInfo.device_id"
>{{ app.selectedInfo.device_id || "-" }}</span
>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
<div
v-for="feat in [
{ key: 'encrypt', label: '加密' },
{ key: 'compress', label: '压缩' },
{ key: 'fec', label: 'FEC纠错' },
{ key: 'rtx', label: 'QUIC传输' },
]"
:key="feat.key"
class="flex items-center gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50"
>
<span
class="h-2 w-2 rounded-full"
:class="app.selectedInfo[feat.key] ? 'bg-green-500' : 'bg-slate-300 dark:bg-slate-600'"
></span>
<span class="text-sm text-slate-600 dark:text-slate-300">{{ feat.label }}</span>
</div>
</div>
<div class="mt-4">
<span class="mb-2 block text-sm muted">Public IPv4s</span>
<div class="flex flex-wrap gap-2">
<span
v-for="pip in app.selectedInfo.public_ipv4s"
:key="pip"
class="rounded border border-slate-200 bg-slate-50 px-2 py-1 font-mono text-xs tabular-nums text-green-700 dark:border-slate-600 dark:bg-slate-800 dark:text-green-300"
>{{ pip }}</span
>
<span
v-if="!app.selectedInfo.public_ipv4s || app.selectedInfo.public_ipv4s.length === 0"
class="text-xs text-slate-400"
>无</span
>
</div>
</div>
</div>
<div class="card p-0 overflow-hidden">
<div class="border-b border-slate-200 px-6 py-4 dark:border-slate-700">
<h2 class="text-lg font-bold text-slate-900 dark:text-white">服务器连接列表</h2>
</div>
<div class="custom-scrollbar max-h-[400px] overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>地址</th>
<th>状态</th>
<th>延迟</th>
<th>版本</th>
</tr>
</thead>
<tbody>
<tr v-for="(server, idx) in app.selectedInfo.server_info" :key="idx">
<td class="font-mono">{{ server.server }}</td>
<td>
<span :class="server.connected ? 'badge-green' : 'badge-red'">
{{ server.connected ? "已连接" : "未连接" }}
</span>
</td>
<td class="tabular-nums">{{ server.server_rtt ? server.server_rtt + " ms" : "-" }}</td>
<td>{{ server.server_version || "-" }}</td>
</tr>
<tr v-if="!app.selectedInfo.server_info || app.selectedInfo.server_info.length === 0">
<td colspan="4" class="text-center text-slate-400">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
-152
View File
@@ -1,152 +0,0 @@
<script setup>
import { useAppStore } from "../stores/app";
import StartPanel from "../components/StartPanel.vue";
import InstanceCard from "../components/InstanceCard.vue";
import EmptyState from "../components/EmptyState.vue";
const app = useAppStore();
</script>
<template>
<div class="space-y-6">
<!-- 页面标题 -->
<div>
<h1 class="page-title">实例</h1>
<p class="page-subtitle">管理组网实例点击卡片查看详情</p>
</div>
<!-- 启动组网 -->
<StartPanel />
<!-- 组网实例 -->
<EmptyState v-if="app.instanceList.length === 0" text="暂无运行中的组网,请在上方选择配置启动" />
<TransitionGroup v-else name="card-list" tag="div" class="grid grid-cols-1 gap-4 md:grid-cols-2">
<InstanceCard v-for="inst in app.instanceList" :key="inst.file_name" :inst="inst" selectable />
</TransitionGroup>
<!-- 选中实例详情 -->
<template v-if="app.selectedInstance && app.selectedInfo">
<div class="card">
<h2 class="mb-4 text-lg font-bold text-slate-900 dark:text-white">
网络详情
<span class="ml-2 text-sm font-medium text-indigo-600 dark:text-indigo-400">{{
app.selectedConfigName
}}</span>
</h2>
<div class="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">虚拟 IP / 掩码</span>
<span class="font-mono tabular-nums text-slate-900 dark:text-white"
>{{ app.selectedInfo.ip || "-" }} / {{ app.selectedInfo.prefix_len || "-" }}</span
>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">网关</span>
<span class="font-mono text-slate-900 dark:text-white">{{ app.selectedInfo.gateway || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">网络编号</span>
<span class="font-mono text-slate-900 dark:text-white">{{ app.selectedInfo.network_code || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">MTU</span>
<span class="font-mono tabular-nums text-slate-900 dark:text-white">{{ app.selectedInfo.mtu || "" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">NAT 类型</span>
<span class="font-mono text-indigo-600 dark:text-indigo-400">{{
app.selectedInfo.nat_type || "Unknown"
}}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">Public IPv6</span>
<span
class="max-w-[200px] truncate font-mono text-slate-900 dark:text-white"
:title="app.selectedInfo.public_ipv6"
>{{ app.selectedInfo.public_ipv6 || "-" }}</span
>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">设备名称</span>
<span class="text-slate-900 dark:text-white">{{ app.selectedInfo.name || "-" }}</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-2 dark:border-slate-700">
<span class="muted">设备 ID</span>
<span
class="max-w-[200px] truncate font-mono text-xs text-slate-500 dark:text-slate-400"
:title="app.selectedInfo.device_id"
>{{ app.selectedInfo.device_id || "-" }}</span
>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
<div
v-for="feat in [
{ key: 'encrypt', label: '加密' },
{ key: 'compress', label: '压缩' },
{ key: 'fec', label: 'FEC纠错' },
{ key: 'rtx', label: 'QUIC传输' },
]"
:key="feat.key"
class="flex items-center gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50"
>
<span
class="h-2 w-2 rounded-full"
:class="app.selectedInfo[feat.key] ? 'bg-green-500' : 'bg-slate-300 dark:bg-slate-600'"
></span>
<span class="text-sm text-slate-600 dark:text-slate-300">{{ feat.label }}</span>
</div>
</div>
<div class="mt-4">
<span class="mb-2 block text-sm muted">Public IPv4s</span>
<div class="flex flex-wrap gap-2">
<span
v-for="pip in app.selectedInfo.public_ipv4s"
:key="pip"
class="rounded border border-slate-200 bg-slate-50 px-2 py-1 font-mono text-xs tabular-nums text-green-700 dark:border-slate-600 dark:bg-slate-800 dark:text-green-300"
>{{ pip }}</span
>
<span
v-if="!app.selectedInfo.public_ipv4s || app.selectedInfo.public_ipv4s.length === 0"
class="text-xs text-slate-400"
>无</span
>
</div>
</div>
</div>
<div class="card p-0 overflow-hidden">
<div class="border-b border-slate-200 px-6 py-4 dark:border-slate-700">
<h2 class="text-lg font-bold text-slate-900 dark:text-white">服务器连接列表</h2>
</div>
<div class="custom-scrollbar max-h-[400px] overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>地址</th>
<th>状态</th>
<th>延迟</th>
<th>版本</th>
</tr>
</thead>
<tbody>
<tr v-for="(server, idx) in app.selectedInfo.server_info" :key="idx">
<td class="font-mono">{{ server.server }}</td>
<td>
<span :class="server.connected ? 'badge-green' : 'badge-red'">
{{ server.connected ? "已连接" : "未连接" }}
</span>
</td>
<td class="tabular-nums">{{ server.server_rtt ? server.server_rtt + " ms" : "-" }}</td>
<td>{{ server.server_version || "-" }}</td>
</tr>
<tr v-if="!app.selectedInfo.server_info || app.selectedInfo.server_info.length === 0">
<td colspan="4" class="text-center text-slate-400">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>