接入六个新页面,外壳加内核状态心跳

导航新增「规则」项;设置、配置、规则页接 saved 事件重读配置,仪表盘只
读传 data。心跳每 10 秒探一次内核在不在:kernel.online 原本只在 load()
里更新,用户停在任何页面期间它都是静态快照,内核重启一次,请求页和
仪表盘会锁死在「内核未连接」,各页等它回来的 watch 永远不可达。恢复
在线的那一次顺带拉新运行时数据。
This commit is contained in:
2026-08-11 21:14:03 +08:00
parent 5bd4e8d609
commit bd087aabea
3 changed files with 57 additions and 4 deletions
+49 -3
View File
@@ -1,11 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ApiError, api, clearToken, getToken, setToken } from './api' import { ApiError, api, clearToken, getToken, setToken } from './api'
import type { ConfigSnapshot } from './api' import type { ConfigSnapshot } from './api'
import { NAV } from './nav' import { NAV } from './nav'
import { density, toggleDensity } from './prefs' import { density, toggleDensity } from './prefs'
import { kernel, reloadRuntime } from './runtime' import { kernel, refreshKernel, reloadRuntime } from './runtime'
import ConfigPage from './views/ConfigPage.vue'
import ConnectionsPage from './views/ConnectionsPage.vue'
import DashboardPage from './views/DashboardPage.vue'
import RequestsPage from './views/RequestsPage.vue'
import RouterPage from './views/RouterPage.vue' import RouterPage from './views/RouterPage.vue'
import RulesPage from './views/RulesPage.vue'
import SettingsPage from './views/SettingsPage.vue'
import TokenGate from './views/TokenGate.vue' import TokenGate from './views/TokenGate.vue'
import IconFold from '~icons/ri/sidebar-fold-line' import IconFold from '~icons/ri/sidebar-fold-line'
import IconUnfold from '~icons/ri/sidebar-unfold-line' import IconUnfold from '~icons/ri/sidebar-unfold-line'
@@ -55,6 +61,24 @@ onMounted(() => {
if (!needsToken.value) void load() if (!needsToken.value) void load()
}) })
/**
* 内核状态的心跳。kernel.online 原本只在 load() 里更新,用户停在任何页面期间它都是
* 静态快照:内核重启一次,请求页和仪表盘会锁死在「内核未连接」,各页等它回来的 watch
* 永远不会触发。10 秒探一次(/api/kernel 是个 5 秒超时的轻量探测),恢复在线的那一次
* 顺带把运行时数据拉新,路由页的组和线路也跟着回来。
*/
let kernelTimer = 0
onMounted(() => {
kernelTimer = window.setInterval(() => {
if (needsToken.value || !snapshot.value) return
const wasOnline = kernel.value.online
void refreshKernel().then(() => {
if (!wasOnline && kernel.value.online) void reloadRuntime()
})
}, 10_000)
})
onBeforeUnmount(() => window.clearInterval(kernelTimer))
const problems = computed( const problems = computed(
() => snapshot.value?.diagnostics.filter((d) => d.severity === 'error') ?? [], () => snapshot.value?.diagnostics.filter((d) => d.severity === 'error') ?? [],
) )
@@ -120,13 +144,35 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
</ul> </ul>
</div> </div>
<DashboardPage v-if="active === 'dashboard'" :data="snapshot.data" />
<RouterPage <RouterPage
v-if="active === 'router'" v-else-if="active === 'router'"
:data="snapshot.data" :data="snapshot.data"
:revision="snapshot.revision" :revision="snapshot.revision"
@saved="load" @saved="load"
/> />
<ConfigPage
v-else-if="active === 'config'"
:data="snapshot.data"
:revision="snapshot.revision"
@saved="load"
/>
<RulesPage
v-else-if="active === 'rules'"
:data="snapshot.data"
:revision="snapshot.revision"
@saved="load"
/>
<RequestsPage v-else-if="active === 'requests'" />
<ConnectionsPage v-else-if="active === 'connections'" />
<SettingsPage v-else-if="active === 'settings'" @saved="load" />
<div v-else class="note"> <div v-else class="note">
<component :is="current?.icon" class="note-icon" /> <component :is="current?.icon" class="note-icon" />
<p>{{ current?.label }} 尚未实现</p> <p>{{ current?.label }} 尚未实现</p>
+2
View File
@@ -6,6 +6,7 @@
import IconRoute from '~icons/ri/route-line' import IconRoute from '~icons/ri/route-line'
import IconDashboard from '~icons/ri/dashboard-line' import IconDashboard from '~icons/ri/dashboard-line'
import IconConfig from '~icons/ri/file-list-3-line' import IconConfig from '~icons/ri/file-list-3-line'
import IconRules from '~icons/ri/filter-3-line'
import IconRequests from '~icons/ri/pulse-line' import IconRequests from '~icons/ri/pulse-line'
import IconConnections from '~icons/ri/links-line' import IconConnections from '~icons/ri/links-line'
import IconSettings from '~icons/ri/settings-3-line' import IconSettings from '~icons/ri/settings-3-line'
@@ -21,6 +22,7 @@ export const NAV: NavItem[] = [
{ key: 'dashboard', label: '仪表盘', icon: IconDashboard }, { key: 'dashboard', label: '仪表盘', icon: IconDashboard },
{ key: 'router', label: '路由', icon: IconRoute }, { key: 'router', label: '路由', icon: IconRoute },
{ key: 'config', label: '配置', icon: IconConfig }, { key: 'config', label: '配置', icon: IconConfig },
{ key: 'rules', label: '规则', icon: IconRules },
{ key: 'requests', label: '请求', icon: IconRequests }, { key: 'requests', label: '请求', icon: IconRequests },
{ key: 'connections', label: '连接', icon: IconConnections }, { key: 'connections', label: '连接', icon: IconConnections },
{ key: 'settings', label: '设置', icon: IconSettings }, { key: 'settings', label: '设置', icon: IconSettings },
+6 -1
View File
@@ -32,6 +32,11 @@ export const lineProvider = ref<Record<string, string>>({})
export const kernel = ref<{ online: boolean; version?: string }>({ online: false }) export const kernel = ref<{ online: boolean; version?: string }>({ online: false })
/** 只探测内核在不在线,不拉运行时数据。给外壳的定时心跳用,各页等内核回来的 watch 靠它可达。 */
export async function refreshKernel(): Promise<void> {
kernel.value = await api.kernel().catch(() => ({ online: false }))
}
/** /**
* 重新读一遍运行时状态。 * 重新读一遍运行时状态。
* *
@@ -42,7 +47,7 @@ export const kernel = ref<{ online: boolean; version?: string }>({ online: false
* 最需要这个界面的时候。 * 最需要这个界面的时候。
*/ */
export async function reloadRuntime(): Promise<void> { export async function reloadRuntime(): Promise<void> {
kernel.value = await api.kernel().catch(() => ({ online: false })) await refreshKernel()
if (!kernel.value.online) { if (!kernel.value.online) {
runtime.value = {} runtime.value = {}
providers.value = {} providers.value = {}