feat(workbench): integrate runtime data and map tooling
This commit is contained in:
@@ -2,18 +2,25 @@ import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
CalendarClock,
|
||||
BarChart3,
|
||||
Clock3,
|
||||
Droplets,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FlaskConical,
|
||||
RadioTower,
|
||||
Sparkles,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
import { MAP_ICON_CELL_RADIUS_CLASS_NAME } from "@/features/map/core";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
|
||||
import { AlertMenu, ScenarioMenu, UserMenu } from "./workbench-top-bar-menus";
|
||||
import type {
|
||||
WorkbenchAlert,
|
||||
WorkbenchAlertDataSource,
|
||||
WorkbenchUser
|
||||
} from "../types";
|
||||
import type { AnalysisArtifact, ArtifactRequestedView } from "../workspace/workspace-model";
|
||||
import { AlertMenu, AnalysisViewMenu, UserMenu } from "./workbench-top-bar-menus";
|
||||
import {
|
||||
compactHeaderTextClassName,
|
||||
headerControlButtonClassName,
|
||||
@@ -23,27 +30,31 @@ import {
|
||||
export type WorkbenchTopBarProps = {
|
||||
dataTime: string;
|
||||
modelName: string;
|
||||
scenarios: WorkbenchScenario[];
|
||||
activeScenarioId: string;
|
||||
activeAnalysisArtifact: AnalysisArtifact | null;
|
||||
alerts: WorkbenchAlert[];
|
||||
alertDataSource: WorkbenchAlertDataSource;
|
||||
user: WorkbenchUser;
|
||||
conditionFeedVisible: boolean;
|
||||
timelineVisible: boolean;
|
||||
taskTickerAvailable: boolean;
|
||||
taskTickerVisible: boolean;
|
||||
analysisResultsPanelOpen: boolean;
|
||||
peripheralSamplePanelOpen: boolean;
|
||||
devPanelEnabled: boolean;
|
||||
devPanelOpen: boolean;
|
||||
onSelectScenario: (scenarioId: string) => void;
|
||||
onSelectAlert: (alert: WorkbenchAlert) => void;
|
||||
onToggleConditionFeed: () => void;
|
||||
onToggleTimeline: () => void;
|
||||
onToggleTaskTicker: () => void;
|
||||
onToggleAnalysisResultsPanel: () => void;
|
||||
onTogglePeripheralSamplePanel: () => void;
|
||||
onToggleDevPanel: () => void;
|
||||
onPreviewScenario: () => void;
|
||||
onCompareScenario: () => void;
|
||||
onExportScenarioReport: () => void;
|
||||
onAnalyzeAlertsWithAgent: () => void | Promise<void>;
|
||||
onCreateTestAnalysis?: () => void;
|
||||
latestAnalysisAvailable: boolean;
|
||||
analysisSplitAvailable: boolean;
|
||||
onOpenLatestAnalysis: () => void;
|
||||
onSetAnalysisView: (view: ArtifactRequestedView) => void;
|
||||
onCloseAnalysisView: () => void;
|
||||
onShowDataStatus: () => void;
|
||||
onRefreshTiles: () => void;
|
||||
onShowShortcuts: () => void;
|
||||
@@ -51,32 +62,36 @@ export type WorkbenchTopBarProps = {
|
||||
onLogout?: () => Promise<void>;
|
||||
};
|
||||
|
||||
type HeaderMenuId = "alerts" | "compact-alerts" | "scenario" | "user";
|
||||
type HeaderMenuId = "alerts" | "analysis-view" | "compact-alerts" | "user";
|
||||
|
||||
export function WorkbenchTopBar({
|
||||
dataTime,
|
||||
modelName,
|
||||
scenarios,
|
||||
activeScenarioId,
|
||||
activeAnalysisArtifact,
|
||||
alerts,
|
||||
alertDataSource,
|
||||
user,
|
||||
conditionFeedVisible,
|
||||
timelineVisible,
|
||||
taskTickerAvailable,
|
||||
taskTickerVisible,
|
||||
analysisResultsPanelOpen,
|
||||
peripheralSamplePanelOpen,
|
||||
devPanelEnabled,
|
||||
devPanelOpen,
|
||||
onSelectScenario,
|
||||
onSelectAlert,
|
||||
onToggleConditionFeed,
|
||||
onToggleTimeline,
|
||||
onToggleTaskTicker,
|
||||
onToggleAnalysisResultsPanel,
|
||||
onTogglePeripheralSamplePanel,
|
||||
onToggleDevPanel,
|
||||
onPreviewScenario,
|
||||
onCompareScenario,
|
||||
onExportScenarioReport,
|
||||
onAnalyzeAlertsWithAgent,
|
||||
onCreateTestAnalysis,
|
||||
latestAnalysisAvailable,
|
||||
analysisSplitAvailable,
|
||||
onOpenLatestAnalysis,
|
||||
onSetAnalysisView,
|
||||
onCloseAnalysisView,
|
||||
onShowDataStatus,
|
||||
onRefreshTiles,
|
||||
onShowShortcuts,
|
||||
@@ -84,15 +99,13 @@ export function WorkbenchTopBar({
|
||||
onLogout
|
||||
}: WorkbenchTopBarProps) {
|
||||
const [openMenu, setOpenMenu] = useState<HeaderMenuId | null>(null);
|
||||
const activeScenario =
|
||||
scenarios.find((scenario) => scenario.id === activeScenarioId) ?? scenarios[0];
|
||||
const createMenuOpenChange = (menuId: HeaderMenuId) => (open: boolean) => {
|
||||
setOpenMenu(open ? menuId : null);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="acrylic-navigation pointer-events-auto absolute left-0 right-0 top-0 z-30 flex h-14 select-none items-center justify-between border-b px-3 sm:px-4 md:px-5">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<header className="acrylic-navigation pointer-events-auto absolute left-0 right-0 top-0 z-30 flex h-14 select-none items-center justify-between gap-2 border-b px-2 sm:gap-3 sm:px-4 md:px-5">
|
||||
<div className="flex min-w-0 items-center gap-2 sm:gap-2.5 lg:shrink-0">
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center bg-blue-600 text-white shadow-lg shadow-blue-600/20",
|
||||
@@ -102,17 +115,20 @@ export function WorkbenchTopBar({
|
||||
<Droplets size={18} aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-sm font-semibold leading-5 text-slate-950">
|
||||
供水管网智能调度系统
|
||||
<h1 aria-label="供水管网智能调度系统" className="truncate text-sm font-semibold leading-5 text-slate-950">
|
||||
<span className="sm:hidden">供水</span>
|
||||
<span className="hidden sm:inline">供水管网智能调度系统</span>
|
||||
</h1>
|
||||
<p className="hidden truncate text-xs leading-4 text-slate-500 sm:block">调度工作台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden min-w-0 items-center gap-2 text-sm text-slate-700 lg:flex 2xl:gap-4">
|
||||
<div className="hidden shrink-0 items-center gap-2 text-sm text-slate-700 lg:flex">
|
||||
<HeaderReadout icon={Clock3} label="数据时间" value={dataTime} />
|
||||
<HeaderDivider />
|
||||
<HeaderReadout icon={Box} label="模型版本" value={modelName} />
|
||||
<div className="hidden items-center gap-2 xl:flex">
|
||||
<HeaderDivider />
|
||||
<HeaderReadout icon={Box} label="模型版本" value={modelName} />
|
||||
</div>
|
||||
<HeaderDivider />
|
||||
<TaskTickerToggle
|
||||
available={taskTickerAvailable}
|
||||
@@ -126,48 +142,77 @@ export function WorkbenchTopBar({
|
||||
open={openMenu === "alerts"}
|
||||
onOpenChange={createMenuOpenChange("alerts")}
|
||||
alerts={alerts}
|
||||
sourceLabel={alertDataSource === "mock" ? "工况演示回退" : "后端运行态"}
|
||||
conditionFeedVisible={conditionFeedVisible}
|
||||
onSelectAlert={onSelectAlert}
|
||||
onToggleConditionFeed={onToggleConditionFeed}
|
||||
onAnalyzeAlertsWithAgent={onAnalyzeAlertsWithAgent}
|
||||
/>
|
||||
<HeaderDivider />
|
||||
{activeScenario ? (
|
||||
<ScenarioMenu
|
||||
open={openMenu === "scenario"}
|
||||
onOpenChange={createMenuOpenChange("scenario")}
|
||||
scenarios={scenarios}
|
||||
activeScenarioId={activeScenario.id}
|
||||
activeScenarioName={activeScenario.name}
|
||||
onSelectScenario={onSelectScenario}
|
||||
onPreviewScenario={onPreviewScenario}
|
||||
onCompareScenario={onCompareScenario}
|
||||
onExportScenarioReport={onExportScenarioReport}
|
||||
/>
|
||||
{activeAnalysisArtifact ? (
|
||||
<>
|
||||
<HeaderDivider />
|
||||
<AnalysisViewMenu
|
||||
open={openMenu === "analysis-view"}
|
||||
onOpenChange={createMenuOpenChange("analysis-view")}
|
||||
title={activeAnalysisArtifact.title}
|
||||
subtitle={activeAnalysisArtifact.subtitle}
|
||||
sourceLabel={getAnalysisOriginLabel(activeAnalysisArtifact.origin)}
|
||||
splitAvailable={analysisSplitAvailable}
|
||||
hasSpatialView={activeAnalysisArtifact.mapRelation !== "none"}
|
||||
onShowResult={() => onSetAnalysisView("focus")}
|
||||
onShowMapSplit={() => onSetAnalysisView("map_split")}
|
||||
onShowMapOnly={() => onSetAnalysisView("map_only")}
|
||||
onClose={onCloseAnalysisView}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="项目分析成果"
|
||||
aria-pressed={analysisResultsPanelOpen}
|
||||
title="项目分析成果"
|
||||
onClick={onToggleAnalysisResultsPanel}
|
||||
className={cn("group px-2", headerControlButtonClassName)}
|
||||
>
|
||||
<span className={headerControlIconClassName}>
|
||||
<BarChart3 size={14} aria-hidden="true" />
|
||||
</span>
|
||||
<span className={cn(compactHeaderTextClassName, "hidden xl:inline")}>分析成果</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="外围样板管理"
|
||||
aria-pressed={peripheralSamplePanelOpen}
|
||||
title="外围样板管理"
|
||||
onClick={onTogglePeripheralSamplePanel}
|
||||
className={cn("group px-2", headerControlButtonClassName)}
|
||||
>
|
||||
<span className={headerControlIconClassName}><RadioTower size={14} aria-hidden="true" /></span>
|
||||
<span className={cn(compactHeaderTextClassName, "hidden xl:inline")}>外围样板</span>
|
||||
</button>
|
||||
<div className="lg:hidden">
|
||||
<TimelineToggle compact visible={timelineVisible} onToggle={onToggleTimeline} />
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<TimelineToggle visible={timelineVisible} onToggle={onToggleTimeline} />
|
||||
</div>
|
||||
{onCreateTestAnalysis ? (
|
||||
{latestAnalysisAvailable && !activeAnalysisArtifact ? (
|
||||
<div className="hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="生成 Agent 模拟分析成果"
|
||||
title="生成 Agent 模拟分析成果"
|
||||
aria-label="打开最新后端分析成果"
|
||||
title="打开最新后端分析成果"
|
||||
data-selection-tone="soft"
|
||||
onClick={onCreateTestAnalysis}
|
||||
onClick={onOpenLatestAnalysis}
|
||||
className={cn("group px-2", headerControlButtonClassName)}
|
||||
>
|
||||
<span className={headerControlIconClassName}>
|
||||
<Sparkles size={14} aria-hidden="true" />
|
||||
</span>
|
||||
<span className={compactHeaderTextClassName}>测试分析</span>
|
||||
<span className="hidden text-sm font-semibold xl:inline">最新分析</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -194,6 +239,7 @@ export function WorkbenchTopBar({
|
||||
open={openMenu === "compact-alerts"}
|
||||
onOpenChange={createMenuOpenChange("compact-alerts")}
|
||||
alerts={alerts}
|
||||
sourceLabel={alertDataSource === "mock" ? "工况演示回退" : "后端运行态"}
|
||||
conditionFeedVisible={conditionFeedVisible}
|
||||
onSelectAlert={onSelectAlert}
|
||||
onToggleConditionFeed={onToggleConditionFeed}
|
||||
@@ -215,6 +261,12 @@ export function WorkbenchTopBar({
|
||||
);
|
||||
}
|
||||
|
||||
function getAnalysisOriginLabel(origin: AnalysisArtifact["origin"]) {
|
||||
if (origin === "backend") return "后端分析运行";
|
||||
if (origin === "demo") return "演示成果";
|
||||
return "Agent 分析";
|
||||
}
|
||||
|
||||
function TaskTickerToggle({
|
||||
available,
|
||||
visible,
|
||||
@@ -306,7 +358,7 @@ function HeaderReadout({
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
||||
{Icon ? <Icon size={14} className="shrink-0 text-blue-700" aria-hidden="true" /> : null}
|
||||
<span className="hidden text-xs font-medium text-slate-500 2xl:inline">{label}</span>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span className="max-w-40 shrink-0 truncate text-sm font-semibold text-slate-800">
|
||||
{value}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user