feat: refine workbench visuals and map controls

Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling.

Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
This commit is contained in:
2026-07-28 16:39:36 +08:00
parent ade88b4fbf
commit f4318a9b9c
96 changed files with 5480 additions and 1875 deletions
@@ -24,6 +24,12 @@ import {
MAP_READABLE_RADIUS_CLASS_NAME
} from "@/features/map/core";
import { cn } from "@/shared/ui/cn";
import {
StatusBadge,
StatusDot,
StatusIcon as SemanticStatusIcon,
type StatusTone
} from "@/shared/ui/status";
import {
DropdownMenu,
DropdownMenuContent,
@@ -36,6 +42,7 @@ import {
} from "@/shared/ui/dropdown-menu";
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
import {
compactHeaderControlButtonClassName,
compactHeaderTextClassName,
headerControlButtonClassName,
headerControlIconClassName
@@ -47,6 +54,12 @@ const scenarioStatusLabels: Record<WorkbenchScenario["status"], string> = {
review: "待复核"
};
const scenarioStatusTones = {
active: "info",
draft: "neutral",
review: "warning"
} satisfies Record<WorkbenchScenario["status"], StatusTone>;
const menuSurfaceClassName = cn(
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
"acrylic-panel border p-2 text-slate-900"
@@ -112,9 +125,9 @@ export function ScenarioMenu({
<span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2">
<span className="truncate font-semibold text-slate-900">{scenario.name}</span>
<span className="shrink-0 rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-xs font-medium text-slate-500">
<StatusBadge tone={scenarioStatusTones[scenario.status]}>
{scenarioStatusLabels[scenario.status]}
</span>
</StatusBadge>
</span>
<span className="mt-1 block text-xs leading-5 text-slate-500">{scenario.description}</span>
</span>
@@ -161,29 +174,38 @@ export function AlertMenu({
className={cn(
"group relative px-2",
headerControlButtonClassName,
hasAlerts && "hover:bg-red-50/60 hover:text-red-800 data-[state=open]:bg-red-50/70 data-[state=open]:text-red-800",
compact && "h-10 w-10 justify-center px-0"
hasAlerts && !compact &&
"status-tone-danger border-transparent bg-[color-mix(in_oklch,var(--status-soft),var(--status-border)_45%)] text-[var(--status-foreground)] [@media(hover:hover)]:hover:border-[var(--status-border)] [@media(hover:hover)]:hover:bg-[color-mix(in_oklch,var(--status-soft),var(--status-border)_75%)] [@media(hover:hover)]:hover:text-[var(--status-foreground)] data-[state=open]:border-transparent data-[state=open]:bg-[color-mix(in_oklch,var(--status-soft),var(--status-border)_75%)] data-[state=open]:text-[var(--status-foreground)]",
compact && compactHeaderControlButtonClassName
)}
aria-label={`查看异常处置面板,当前 ${alerts.length} 条待复核工况`}
>
<span
className={cn(
headerControlIconClassName,
hasAlerts &&
"bg-red-50 text-red-700 group-hover:bg-red-50 group-hover:text-red-800 group-data-[state=open]:bg-red-50 group-data-[state=open]:text-red-800"
hasAlerts ? "status-icon status-tone-danger" : "bg-transparent!"
)}
>
<Bell size={14} aria-hidden="true" />
</span>
<span className={compact ? "sr-only" : compactHeaderTextClassName}>异常处置</span>
{compact ? (
<span className={cn("absolute right-0 top-0 grid h-4 min-w-4 place-items-center rounded-full border px-1 text-xs font-semibold leading-none", hasAlerts ? "border-red-100 bg-red-50 text-red-800" : "border-slate-200 bg-slate-50 text-slate-500")}>
<StatusBadge
tone={hasAlerts ? "danger" : "neutral"}
className={cn(
"absolute right-0 top-0 min-h-4 min-w-4 justify-center px-1 py-0 leading-none",
!hasAlerts && "border-transparent! bg-transparent!"
)}
>
{alerts.length}
</span>
</StatusBadge>
) : (
<span className={cn("rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-none", hasAlerts ? "border-red-100 bg-red-50 text-red-800" : "border-slate-200 bg-slate-50 text-slate-500")}>
<StatusBadge
tone={hasAlerts ? "danger" : "neutral"}
className={cn("leading-none", !hasAlerts && "border-transparent! bg-transparent!")}
>
{alerts.length}
</span>
</StatusBadge>
)}
</button>
</DropdownMenuTrigger>
@@ -199,7 +221,7 @@ export function AlertMenu({
<DropdownMenuItem
key={alert.id}
className={cn(
"group my-1 grid min-h-[72px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border border-transparent px-2.5 py-2.5 transition-colors focus:border-red-100",
"status-tone-danger group my-1 grid min-h-[72px] grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border border-transparent px-2.5 py-2.5 transition-colors focus:border-[var(--status-border)]",
MAP_COMPACT_RADIUS_CLASS_NAME,
menuReadableRowClassName
)}
@@ -207,20 +229,22 @@ export function AlertMenu({
>
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-2">
<span className="h-2 w-2 shrink-0 rounded-full bg-red-300 ring-2 ring-red-50" aria-hidden="true" />
<StatusDot tone="danger" size="md" />
<span className="min-w-0 flex-1 truncate text-sm font-semibold leading-5 text-slate-950">{alert.title}</span>
<span className="shrink-0 font-mono text-xs font-semibold leading-4 text-slate-400 tabular-nums">{alert.time}</span>
</span>
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{alert.description}</span>
</span>
<span className={cn("grid h-7 w-7 shrink-0 place-items-center bg-slate-50 text-slate-400 transition-colors group-focus:text-red-700", MAP_ICON_CELL_RADIUS_CLASS_NAME)} aria-hidden="true">
<span className={cn("status-tone-danger grid h-7 w-7 shrink-0 place-items-center bg-slate-50 text-slate-400 transition-colors group-focus:text-[var(--status-foreground)]", MAP_ICON_CELL_RADIUS_CLASS_NAME)} aria-hidden="true">
<ChevronRight size={14} />
</span>
</DropdownMenuItem>
)) : (
<div className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-slate-200 px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
<div className="min-w-0">
<CheckCircle2 size={18} className="mx-auto text-emerald-600" aria-hidden="true" />
<SemanticStatusIcon tone="success" size="md" className="mx-auto">
<CheckCircle2 size={16} />
</SemanticStatusIcon>
<p className="mt-1 text-xs font-semibold text-slate-700">当前无待复核工况</p>
<p className="mt-0.5 text-xs leading-4 text-slate-500">新的工况提醒会进入这里。</p>
</div>
@@ -274,7 +298,11 @@ export function UserMenu({
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn("group px-2", headerControlButtonClassName)}
className={cn(
"group lg:h-8 lg:w-auto lg:justify-start lg:px-2",
headerControlButtonClassName,
compactHeaderControlButtonClassName
)}
aria-label="打开用户菜单"
>
<span className={headerControlIconClassName}>
@@ -316,15 +344,13 @@ function AlertQueueSummary({
<div className={cn("surface-reading overflow-hidden px-3 py-3", MAP_READABLE_RADIUS_CLASS_NAME)}>
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-start gap-2.5">
<span
className={cn(
"grid h-8 w-8 shrink-0 place-items-center",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
hasAlerts ? "bg-red-50 text-red-700" : "bg-emerald-50 text-emerald-700"
)}
<SemanticStatusIcon
tone={hasAlerts ? "danger" : "success"}
size="md"
className={cn("h-8 w-8", MAP_ICON_CELL_RADIUS_CLASS_NAME)}
>
{hasAlerts ? <Bell size={16} aria-hidden="true" /> : <CheckCircle2 size={16} aria-hidden="true" />}
</span>
{hasAlerts ? <Bell size={16} /> : <CheckCircle2 size={16} />}
</SemanticStatusIcon>
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold leading-5 text-slate-950">异常处置</h2>
<p className="mt-0.5 text-xs leading-5 text-slate-500">
@@ -332,17 +358,12 @@ function AlertQueueSummary({
</p>
</div>
</div>
<span
className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-xs font-semibold leading-5",
hasAlerts ? "bg-red-50 text-red-800 ring-1 ring-red-100" : "bg-emerald-50 text-emerald-700 ring-1 ring-emerald-100"
)}
>
<StatusBadge tone={hasAlerts ? "danger" : "success"}>
{hasAlerts ? "需复核" : "正常"}
</span>
</StatusBadge>
</div>
<div className="mt-2 flex items-center gap-1.5 text-xs leading-4 text-slate-500">
<span className={cn("h-1.5 w-1.5 rounded-full", conditionFeedVisible ? "bg-blue-500" : "bg-slate-300")} aria-hidden="true" />
<StatusDot tone={conditionFeedVisible ? "info" : "neutral"} />
<span className="truncate">{conditionFeedVisible ? "工况面板已显示,可查看详情。" : "点击条目可打开工况任务。"}</span>
</div>
</div>
@@ -427,9 +448,9 @@ function MenuHeader({
{Icon ? <Icon size={13} className="shrink-0 text-slate-500" aria-hidden="true" /> : null}
<span className="truncate text-sm font-semibold text-slate-950">{title}</span>
</div>
<p className={cn("mt-0.5 truncate text-xs text-slate-500", tone === "warning" && "text-red-600")}>{meta}</p>
<p className={cn("mt-0.5 truncate text-xs text-slate-500", tone === "warning" && "status-tone-danger text-[var(--status-foreground)]")}>{meta}</p>
</div>
{tone === "warning" ? <span className="h-2 w-2 shrink-0 rounded-full bg-red-500" aria-hidden="true" /> : null}
{tone === "warning" ? <StatusDot tone="danger" size="md" /> : null}
</div>
);
}
@@ -469,7 +490,7 @@ function MenuAction({
"grid h-7 w-7 shrink-0 place-items-center border",
MAP_ICON_CELL_RADIUS_CLASS_NAME,
tone === "primary" && "border-blue-100 bg-blue-50 text-blue-700",
tone === "warning" && "border-orange-100 bg-orange-50 text-orange-700",
tone === "warning" && "status-icon status-tone-warning",
tone === "default" && "border-slate-100 bg-slate-50 text-slate-600"
)}
>