refactor(console-web): centralize page lifecycle and UI composition

Declare navigation, page metadata, lazy loaders, and keep-alive policy in one registry. Retain primary iframe pages until a continuous inactive TTL expires and expose typed show, hide, and unload events through the Page SDK.

Move business-aware components out of the UI primitive directory, migrate views and Host dialogs to variant primitives and local Tailwind classes, and remove the global component style layer. Keep native dialogs inside the overlay flex context so they remain centered.

Cover iframe expiration, lifecycle dispatch, UI dependency direction, and global style ownership with focused tests.
This commit is contained in:
Maofeng
2026-07-30 15:16:02 +08:00
parent 23fa6137ff
commit 9ff3223c44
30 changed files with 1136 additions and 872 deletions
@@ -11,8 +11,11 @@ import {
Zap,
} from "lucide-solid";
import { createMemo, For, Show, type JSXElement } from "solid-js";
import { PageHeading } from "../../components/ui/page-heading";
import { ServiceTable } from "../../components/ui/service-table";
import { Page } from "../../components/layout/page";
import { PageHeading } from "../../components/layout/page-heading";
import { ServiceTable } from "../../components/services/service-table";
import { Button, IconButton } from "../../components/ui/button";
import { Card, CardHeader } from "../../components/ui/card";
import { eventsFrom, servicesFrom } from "../../lib/view-state";
import { formatDuration } from "../../lib/model";
import type { PageProps } from "../types";
@@ -29,7 +32,7 @@ export default function OverviewPage(props: PageProps) {
const errors = createMemo(() => services().reduce((total, service) => total + service.errors, 0));
return (
<main class="page">
<Page>
<Show when={props.state()} fallback={<OverviewSkeleton />}>
{(state) => (
<>
@@ -39,28 +42,28 @@ export default function OverviewPage(props: PageProps) {
description={`${services().length} 个已注册版本,${running()} 个 Actor 正在运行。`}
actions={
<>
<button
class="icon-btn"
type="button"
<IconButton
aria-label="刷新"
title="刷新"
onClick={() => props.command({ type: "refresh" })}
>
<RefreshCw size={16} />
</button>
<button
class="btn btn-primary"
type="button"
</IconButton>
<Button
variant="primary"
onClick={() => props.command({ type: "open-register" })}
>
<CloudUpload size={16} />
</button>
</Button>
</>
}
/>
<section class="metric-grid" aria-label="关键指标">
<section
class="mb-5 grid grid-cols-1 border-l border-t border-line sm:grid-cols-2 xl:grid-cols-4"
aria-label="关键指标"
>
<Metric
icon={<Package size={18} />}
tone="bg-cyan-soft text-cyan-strong"
@@ -91,7 +94,10 @@ export default function OverviewPage(props: PageProps) {
/>
</section>
<section class="panel mb-5 grid lg:grid-cols-[minmax(280px,1.4fr)_repeat(3,minmax(130px,1fr))]">
<Card
as="section"
class="mb-5 grid lg:grid-cols-[minmax(280px,1.4fr)_repeat(3,minmax(130px,1fr))]"
>
<div class="flex items-center gap-3 border-b border-line px-5 py-4 lg:border-b-0 lg:border-r">
<span class="flex size-10 items-center justify-center rounded-md bg-brand-soft text-brand">
<Cpu size={18} />
@@ -108,32 +114,33 @@ export default function OverviewPage(props: PageProps) {
<Show
when={state().snapshot?.runtime.status === "running"}
fallback={
<button
class="icon-btn"
<IconButton
aria-label="启动 Runtime"
title="启动 Runtime"
disabled={state().runtimeAction !== null}
onClick={() => props.command({ type: "runtime-action", action: "start" })}
>
<Play size={15} />
</button>
</IconButton>
}
>
<button
class="icon-btn"
<IconButton
aria-label="重启 Runtime"
title="重启 Runtime"
disabled={state().runtimeAction !== null}
onClick={() => props.command({ type: "runtime-action", action: "restart" })}
>
<RotateCcw size={15} />
</button>
<button
class="icon-btn text-coral-strong"
</IconButton>
<IconButton
tone="danger"
aria-label="停止 Runtime"
title="停止 Runtime"
disabled={state().runtimeAction !== null}
onClick={() => props.command({ type: "runtime-action", action: "stop" })}
>
<CircleStop size={15} />
</button>
</IconButton>
</Show>
</div>
</div>
@@ -156,32 +163,32 @@ export default function OverviewPage(props: PageProps) {
}
detail="当前 Runtime"
/>
</section>
</Card>
<div class="grid gap-5 xl:grid-cols-[minmax(0,1.7fr)_minmax(300px,0.8fr)]">
<section class="panel min-w-0">
<div class="panel-header">
<Card as="section" class="min-w-0">
<CardHeader>
<div>
<h2></h2>
<p> Actor </p>
</div>
<button
class="btn btn-secondary h-9"
<Button
size="small"
onClick={() => props.command({ type: "navigate", view: "services" })}
>
</button>
</div>
</Button>
</CardHeader>
<ServiceTable
services={services().slice(0, 5)}
state={state()}
command={props.command}
compact
/>
</section>
</Card>
<aside class="panel">
<div class="panel-header">
<Card as="aside">
<CardHeader>
<div>
<h2></h2>
<p></p>
@@ -190,7 +197,7 @@ export default function OverviewPage(props: PageProps) {
<span class="size-1.5 rounded-full bg-brand" />
LIVE
</span>
</div>
</CardHeader>
<div class="divide-y divide-line">
<For
each={events().slice(0, 6)}
@@ -207,12 +214,12 @@ export default function OverviewPage(props: PageProps) {
)}
</For>
</div>
</aside>
</Card>
</div>
</>
)}
</Show>
</main>
</Page>
);
}
@@ -224,13 +231,15 @@ function Metric(props: {
note: string;
}) {
return (
<article class="metric">
<div class={`metric-icon ${props.tone}`}>{props.icon}</div>
<div class="metric-copy">
<span>{props.label}</span>
<strong>{props.value}</strong>
<article class="flex min-h-24 items-center gap-3 border-b border-r border-line bg-white px-4 py-3">
<div class={`flex size-10 shrink-0 items-center justify-center rounded-md ${props.tone}`}>
{props.icon}
</div>
<small>{props.note}</small>
<div class="min-w-0 flex-1">
<span class="block text-xs text-muted">{props.label}</span>
<strong class="mt-1 block text-2xl font-bold">{props.value}</strong>
</div>
<small class="self-end whitespace-nowrap pb-1 text-[11px] text-muted">{props.note}</small>
</article>
);
}
@@ -248,8 +257,8 @@ function RuntimeStat(props: { label: string; value: string; detail: string }) {
function OverviewSkeleton() {
return (
<div class="space-y-5 py-5">
<div class="skeleton-line w-36" />
<div class="skeleton-line h-8 w-64" />
<div class="h-3 w-36 animate-pulse rounded-sm bg-[#dfe6e4]" />
<div class="h-8 w-64 animate-pulse rounded-sm bg-[#dfe6e4]" />
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
<For each={[1, 2, 3, 4]}>{() => <div class="h-24 animate-pulse bg-white" />}</For>
</div>