feat(web): W3-1 shared console routes, nav, api/sse/query-keys and shared components
- routes.ts registers all workspaces under /console (+ detail routes, engine, audit, login) - api.ts: cookie session first (credentials include, CSRF header), Bearer only with stored key, 401 -> /login - sse.ts: shared EventSource with ?after=seq catch-up, backoff reconnect, per-type subscribe - shared components: data-table, filter-bar, detail-sheet, timeline, json-view, confirm-dialog, form-field, kv-list, stat-tile, error/loading states; ui tabs and textarea Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a24bda952b
commit
e5e42b1f0d
32 files changed
+1286
-71
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
import type { StatusTone } from "./status-badge.tsx";
|
||||
|
||||
export type TimelineItem = {
|
||||
id: string;
|
||||
/** ISO 时间;显示为本地时间 */
|
||||
at?: string | null;
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
/** 圆点语气,与 StatusBadge 一致;标题里仍要有文字说明状态 */
|
||||
tone?: StatusTone;
|
||||
/** 右侧附加信息(耗时、次数等) */
|
||||
meta?: ReactNode;
|
||||
/** 展开区(json-view 等) */
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const DOT: Record<StatusTone, string> = {
|
||||
success: "bg-success",
|
||||
warning: "bg-warning",
|
||||
destructive: "bg-destructive",
|
||||
neutral: "bg-muted-foreground/60",
|
||||
};
|
||||
|
||||
/** 纵向时间线(业务时间线、步骤尝试、审计轨迹)。items 按展示顺序传入 */
|
||||
export function Timeline({ items, className }: { items: TimelineItem[]; className?: string }) {
|
||||
return (
|
||||
<ol className={cn("relative space-y-4", className)}>
|
||||
{items.map((it, i) => (
|
||||
<li key={it.id} className="relative flex gap-3 pl-1">
|
||||
{i < items.length - 1 ? (
|
||||
<span aria-hidden className="absolute top-4 bottom-[-1rem] left-[0.6875rem] w-px bg-border" />
|
||||
) : null}
|
||||
<span aria-hidden className="relative mt-1.5 grid h-3 w-3 shrink-0 place-items-center">
|
||||
<span className={cn("h-2.5 w-2.5 rounded-full ring-4 ring-background", DOT[it.tone ?? "neutral"])} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
||||
<div className="min-w-0 text-sm font-medium break-words text-foreground">{it.title}</div>
|
||||
{it.meta ? <div className="shrink-0 text-meta text-muted-foreground">{it.meta}</div> : null}
|
||||
</div>
|
||||
{it.at ? <p className="text-meta text-muted-foreground tabular-nums">{formatDateTime(it.at)}</p> : null}
|
||||
{it.description ? (
|
||||
<div className="mt-1 text-caption break-words text-muted-foreground">{it.description}</div>
|
||||
) : null}
|
||||
{it.children ? <div className="mt-2">{it.children}</div> : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user