fix: improve mobile workbench controls

This commit is contained in:
2026-07-21 12:35:36 +08:00
parent caf06a8a82
commit ebe6cdc7f1
11 changed files with 144 additions and 62 deletions
@@ -37,6 +37,11 @@ import {
DropdownMenuTrigger
} from "@/shared/ui/dropdown-menu";
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
import {
compactHeaderTextClassName,
headerControlButtonClassName,
headerControlIconClassName
} from "./workbench-top-bar-styles";
const scenarioStatusLabels: Record<WorkbenchScenario["status"], string> = {
active: "运行中",
@@ -50,11 +55,6 @@ const menuSurfaceClassName = cn(
);
const menuReadableRowClassName =
"surface-well focus:bg-blue-50 focus:text-slate-950";
const headerControlButtonClassName =
"inline-flex h-8 items-center gap-2 rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
const headerControlIconClassName =
"grid h-5 w-5 shrink-0 place-items-center rounded-full bg-slate-100/80 text-slate-500 transition-colors group-hover:bg-blue-50 group-hover:text-blue-700 group-data-[state=open]:bg-blue-50 group-data-[state=open]:text-blue-700";
export function ScenarioMenu({
open,
onOpenChange,
@@ -87,8 +87,8 @@ export function ScenarioMenu({
<span className={headerControlIconClassName}>
<SlidersHorizontal size={14} aria-hidden="true" />
</span>
<span className="truncate"> · {activeScenarioName}</span>
<ChevronDown size={14} className="shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600" aria-hidden="true" />
<span className={cn(compactHeaderTextClassName, "truncate")}> · {activeScenarioName}</span>
<ChevronDown size={14} className="hidden shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600 2xl:block" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className={cn("w-[340px]", menuSurfaceClassName)}>
@@ -177,7 +177,7 @@ export function AlertMenu({
>
<Bell size={14} aria-hidden="true" />
</span>
<span className={compact ? "sr-only" : undefined}></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")}>
{alerts.length}
@@ -282,8 +282,8 @@ export function UserMenu({
<span className={headerControlIconClassName}>
<UserRound size={14} aria-hidden="true" />
</span>
<span className="hidden text-sm font-semibold md:inline">{user.name}</span>
<ChevronDown size={14} className="hidden shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600 sm:block" aria-hidden="true" />
<span className={cn(compactHeaderTextClassName, "text-sm font-semibold")}>{user.name}</span>
<ChevronDown size={14} className="hidden shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600 2xl:block" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className={cn("w-72", menuSurfaceClassName)}>
@@ -0,0 +1,7 @@
export const headerControlButtonClassName =
"inline-flex h-8 items-center gap-2 whitespace-nowrap rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
export const headerControlIconClassName =
"grid h-5 w-5 shrink-0 place-items-center rounded-full bg-slate-100/80 text-slate-500 transition-colors group-hover:bg-blue-50 group-hover:text-blue-700 group-data-[state=open]:bg-blue-50 group-data-[state=open]:text-blue-700";
export const compactHeaderTextClassName = "hidden 2xl:inline";
@@ -17,6 +17,11 @@ import {
import { cn } from "@/lib/utils";
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
import { AlertMenu, ScenarioMenu, UserMenu } from "./workbench-top-bar-menus";
import {
compactHeaderTextClassName,
headerControlButtonClassName,
headerControlIconClassName
} from "./workbench-top-bar-styles";
export type WorkbenchTopBarProps = {
dataTime: string;
@@ -45,11 +50,6 @@ export type WorkbenchTopBarProps = {
onExportConfig: () => void;
};
const headerControlButtonClassName =
"inline-flex h-8 items-center gap-2 rounded-full border border-transparent bg-transparent text-sm font-semibold text-slate-700 transition-colors hover:bg-white/70 hover:text-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 focus-visible:ring-offset-2 data-[state=open]:bg-white/90 data-[state=open]:text-blue-700";
const headerControlIconClassName =
"grid h-5 w-5 shrink-0 place-items-center rounded-full bg-slate-100/80 text-slate-500 transition-colors group-hover:bg-blue-50 group-hover:text-blue-700 group-data-[state=open]:bg-blue-50 group-data-[state=open]:text-blue-700";
type HeaderMenuId = "alerts" | "compact-alerts" | "scenario" | "user";
export function WorkbenchTopBar({
@@ -96,7 +96,7 @@ export function WorkbenchTopBar({
</div>
</div>
<div className="hidden min-w-0 items-center gap-3 text-sm text-slate-700 lg:flex xl:gap-4">
<div className="hidden min-w-0 items-center gap-2 text-sm text-slate-700 lg:flex 2xl:gap-4">
<HeaderReadout icon={Clock3} label="数据时间" value={dataTime} />
<HeaderDivider />
<HeaderReadout icon={Box} label="模型版本" value={modelName} />
@@ -144,7 +144,7 @@ export function WorkbenchTopBar({
className={cn("group px-2 active:scale-95", headerControlButtonClassName, devPanelOpen && "bg-blue-50/80 text-blue-700")}
>
<span className={cn(headerControlIconClassName, devPanelOpen && "bg-blue-600/10 text-blue-700")}><FlaskConical size={14} aria-hidden="true" /></span>
<span>Dev</span>
<span className={compactHeaderTextClassName}>Dev</span>
</button>
</div> : null}
<div className="lg:hidden">
@@ -209,7 +209,7 @@ function TaskTickerToggle({
>
<Icon size={14} aria-hidden="true" />
</span>
<span></span>
<span className={compactHeaderTextClassName}></span>
</button>
);
}
@@ -240,7 +240,7 @@ function ConditionFeedToggle({
>
<CalendarClock size={14} aria-hidden="true" />
</span>
<span></span>
<span className={compactHeaderTextClassName}></span>
</button>
);
}
@@ -249,8 +249,8 @@ function HeaderReadout({ icon: Icon, label, value }: { icon?: LucideIcon; label:
return (
<div className="flex min-w-0 items-center gap-1.5 whitespace-nowrap">
{Icon ? <Icon size={14} className="shrink-0 text-blue-700" aria-hidden="true" /> : null}
<span className="text-xs font-medium text-slate-500">{label}</span>
<span className="truncate text-sm font-semibold text-slate-800">{value}</span>
<span className="hidden text-xs font-medium text-slate-500 2xl:inline">{label}</span>
<span className="max-w-16 truncate text-sm font-semibold text-slate-800 2xl:max-w-28">{value}</span>
</div>
);
}
+22 -34
View File
@@ -12,7 +12,6 @@ import {
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent
} from "react";
import { X } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
AgentCollapsedRail,
@@ -220,6 +219,15 @@ export function MapWorkbenchPage() {
onUiEnvelope: handleAgentUiEnvelope,
onFrontendAction: handleFrontendAction
});
function openAgentPanelForViewport() {
if (isLargeScreen) {
agent.expandPanel();
return;
}
agent.openMobilePanel();
}
const leftPanelOpen = isLargeScreen && agent.panelOpen;
const rightPanelOpen = isLargeScreen && (devPanelOpen || shouldShowConditionFeed);
@@ -488,7 +496,7 @@ export function MapWorkbenchPage() {
return;
}
agent.expandPanel();
openAgentPanelForViewport();
try {
await agent.submitPrompt(
@@ -931,7 +939,7 @@ export function MapWorkbenchPage() {
onExpandedChange={setConditionFeedExpanded}
onFocusRequestHandled={handleConditionFocusRequestHandled}
onSelectedConditionChange={setSelectedConditionId}
onExpandAgent={agent.expandPanel}
onExpandAgent={openAgentPanelForViewport}
onLoadHistorySession={handleLoadAgentHistorySession}
onSubmitPrompt={agent.submitPrompt}
/>
@@ -963,14 +971,14 @@ export function MapWorkbenchPage() {
) : null}
</div>
<MobileAgentToggle
open={agent.mobileOpen}
personaState={agent.personaState}
statusLabel={agent.statusLabel}
streaming={agent.streaming}
onOpen={agent.openMobilePanel}
onClose={agent.closeMobilePanel}
/>
{!agent.mobileOpen ? (
<MobileAgentToggle
personaState={agent.personaState}
statusLabel={agent.statusLabel}
streaming={agent.streaming}
onOpen={agent.openMobilePanel}
/>
) : null}
{!agent.mobileOpen ? (
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
@@ -1111,36 +1119,16 @@ function getScheduledConditionIdFromAlertId(alertId: string) {
}
function MobileAgentToggle({
open,
personaState,
statusLabel,
streaming,
onOpen,
onClose
onOpen
}: {
open: boolean;
personaState: ComponentProps<typeof AgentPersona>["state"];
statusLabel: string;
streaming: boolean;
onOpen: () => void;
onClose: () => void;
}) {
if (open) {
return (
<div className="absolute bottom-20 right-3 z-40 lg:hidden">
<button
type="button"
aria-label="关闭 Agent 面板"
title="关闭 Agent 面板"
onClick={onClose}
className="agent-panel-icon-button grid h-10 w-10 place-items-center rounded-2xl text-slate-700 transition"
>
<X size={18} aria-hidden="true" />
</button>
</div>
);
}
return (
<div className="absolute bottom-20 left-3 z-30 lg:hidden">
<button
@@ -1155,8 +1143,8 @@ function MobileAgentToggle({
)}
>
<AgentPersona
className="h-10 w-10"
fallbackClassName="h-10 w-10"
className="pointer-events-none h-10 w-10"
fallbackClassName="pointer-events-none h-10 w-10"
state={personaState}
statusLabel={statusLabel}
streaming={streaming}
+1 -1
View File
@@ -65,7 +65,7 @@ describe("fitNetworkBounds", () => {
expect(options).toMatchObject({
duration: 600,
maxZoom: 13,
padding: { top: 48, right: 48, bottom: 48, left: 48 }
padding: { top: 96, right: 48, bottom: 48, left: 48 }
});
expect(options).not.toHaveProperty("zoom");
});
+1 -1
View File
@@ -13,7 +13,7 @@ export type NetworkViewParams = {
const WEB_MERCATOR_RADIUS = 6378137;
const GLOBAL_VIEW_MAX_ZOOM = 13;
const GLOBAL_VIEW_PADDING: PaddingOptions = { top: 48, right: 48, bottom: 48, left: 48 };
const GLOBAL_VIEW_PADDING: PaddingOptions = { top: 96, right: 48, bottom: 48, left: 48 };
export function getWorkbenchPadding(leftPanelOpen: boolean, rightPanelOpen: boolean): PaddingOptions {
return getWorkbenchCameraPadding(1280, {
agentOpen: leftPanelOpen,