Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28287a1447 |
@@ -5,7 +5,8 @@ import {
|
||||
getAgentPanelMaxWidth,
|
||||
getWorkbenchBasemapTone,
|
||||
getWorkbenchCameraPadding,
|
||||
getWorkbenchViewportLayout
|
||||
getWorkbenchViewportLayout,
|
||||
resolveMobileWorkbenchOverlayLayout
|
||||
} from "./workbench-layout";
|
||||
|
||||
describe("workbench basemap surface tone", () => {
|
||||
@@ -20,6 +21,19 @@ describe("workbench basemap surface tone", () => {
|
||||
});
|
||||
|
||||
describe("workbench floating panels", () => {
|
||||
it("derives mobile dock and map control offsets from one overlay stack", () => {
|
||||
expect(resolveMobileWorkbenchOverlayLayout(true)).toEqual({
|
||||
timelineBottom: 12,
|
||||
dockBottom: 68,
|
||||
mapControlBottom: 120
|
||||
});
|
||||
expect(resolveMobileWorkbenchOverlayLayout(false)).toEqual({
|
||||
timelineBottom: 12,
|
||||
dockBottom: 12,
|
||||
mapControlBottom: 64
|
||||
});
|
||||
});
|
||||
|
||||
it("uses bounded desktop and wide panel widths", () => {
|
||||
expect(getWorkbenchViewportLayout(1440)).toMatchObject({
|
||||
agentWidth: 500,
|
||||
|
||||
@@ -26,9 +26,29 @@ export const WORKBENCH_LAYOUT = {
|
||||
mobileSheet: {
|
||||
halfViewportRatio: 0.52,
|
||||
fullTopGap: 68
|
||||
},
|
||||
mobileOverlay: {
|
||||
edgeInset: 12,
|
||||
gap: 8,
|
||||
dockHeight: 44,
|
||||
timelineSummaryHeight: 48
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function resolveMobileWorkbenchOverlayLayout(timelineVisible: boolean) {
|
||||
const { edgeInset, gap, dockHeight, timelineSummaryHeight } =
|
||||
WORKBENCH_LAYOUT.mobileOverlay;
|
||||
const dockBottom = timelineVisible
|
||||
? edgeInset + timelineSummaryHeight + gap
|
||||
: edgeInset;
|
||||
|
||||
return {
|
||||
timelineBottom: edgeInset,
|
||||
dockBottom,
|
||||
mapControlBottom: dockBottom + dockHeight + gap
|
||||
};
|
||||
}
|
||||
|
||||
export function clampAgentPanelWidth(
|
||||
width: number,
|
||||
viewportWidth: number,
|
||||
|
||||
@@ -78,7 +78,8 @@ import {
|
||||
import {
|
||||
WORKBENCH_LAYOUT,
|
||||
getWorkbenchBasemapTone,
|
||||
getWorkbenchLayoutCssVariables
|
||||
getWorkbenchLayoutCssVariables,
|
||||
resolveMobileWorkbenchOverlayLayout
|
||||
} from "./layout/workbench-layout";
|
||||
import { getResponsiveWorkbenchPadding } from "./map/camera";
|
||||
import { exportMapViewImage } from "./map/export-view";
|
||||
@@ -220,9 +221,10 @@ export function MapWorkbenchPage({
|
||||
timelineMode,
|
||||
timelineVisible
|
||||
);
|
||||
const mobileOverlayLayout = resolveMobileWorkbenchOverlayLayout(timelineVisible);
|
||||
const timelineBottom = isLargeScreen
|
||||
? timelineScaleLayout.timelineBottom
|
||||
: 12;
|
||||
: mobileOverlayLayout.timelineBottom;
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineMode((current) => {
|
||||
@@ -987,19 +989,30 @@ export function MapWorkbenchPage({
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTimelineForViewport}
|
||||
className="acrylic-control flex h-12 w-full items-center gap-2 rounded-2xl border px-3 text-left text-xs text-slate-700 lg:hidden"
|
||||
<div
|
||||
role="region"
|
||||
aria-label="移动端运行时间轴摘要"
|
||||
className="acrylic-control flex h-12 w-full items-center rounded-2xl border p-1 text-xs text-slate-700 lg:hidden"
|
||||
>
|
||||
<Clock3 size={15} className="text-blue-600" aria-hidden="true" />
|
||||
<span className="font-semibold">运行时间轴</span>
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
<span className="ml-auto text-slate-500">查看全天</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开完整运行时间轴"
|
||||
onClick={openTimelineForViewport}
|
||||
className="flex h-10 min-w-0 flex-1 items-center gap-1.5 rounded-xl px-2 text-left transition-colors hover:bg-white/55 active:bg-white/75"
|
||||
>
|
||||
<Clock3 size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<span className="shrink-0 font-semibold">运行时间轴</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 whitespace-nowrap text-[10px]">
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
</span>
|
||||
<span className="ml-auto hidden shrink-0 text-slate-500 min-[360px]:inline">
|
||||
查看全天
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</> : null
|
||||
)}
|
||||
mapContent={(
|
||||
@@ -1019,10 +1032,13 @@ export function MapWorkbenchPage({
|
||||
<ToolbarPanel {...toolbarPanelProps} />
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-0 right-0 flex flex-col items-end gap-2"
|
||||
style={{ bottom: timelineScaleLayout.scaleBottom }}
|
||||
className="absolute right-3 flex flex-col items-end gap-2 bottom-[var(--mobile-map-control-bottom)] transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:right-0 lg:bottom-[var(--desktop-map-scale-bottom)]"
|
||||
style={{
|
||||
"--mobile-map-control-bottom": `${mobileOverlayLayout.mapControlBottom}px`,
|
||||
"--desktop-map-scale-bottom": `${timelineScaleLayout.scaleBottom}px`
|
||||
} as CSSProperties}
|
||||
>
|
||||
<div className="flex items-end gap-2 pr-2">
|
||||
<div className="flex items-end gap-2 lg:pr-2">
|
||||
<MapZoom mapRef={mapRef} mapReady={mapReady} onHome={fitNetworkBounds} />
|
||||
</div>
|
||||
<MapScaleLine
|
||||
@@ -1077,7 +1093,10 @@ export function MapWorkbenchPage({
|
||||
) : null}
|
||||
|
||||
{mobileSheet === null && artifactWorkspace.surfaceMode !== "focus" ? (
|
||||
<div className="absolute bottom-16 left-3 right-3 z-30 lg:hidden">
|
||||
<div
|
||||
className="absolute left-3 right-3 z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden"
|
||||
style={{ bottom: mobileOverlayLayout.mapControlBottom }}
|
||||
>
|
||||
<ToolbarPanel
|
||||
{...toolbarPanelProps}
|
||||
panelWidthClassName="w-full max-h-[52dvh] overflow-y-auto"
|
||||
@@ -1108,7 +1127,10 @@ export function MapWorkbenchPage({
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={`absolute z-30 lg:hidden ${timelineVisible ? "bottom-[4.25rem]" : "bottom-3"} ${artifactWorkspace.surfaceMode === "focus" ? "right-3" : "left-3 right-3"}`}>
|
||||
<div
|
||||
className={`absolute z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden ${artifactWorkspace.surfaceMode === "focus" ? "right-3" : "left-3 right-3"}`}
|
||||
style={{ bottom: mobileOverlayLayout.dockBottom }}
|
||||
>
|
||||
{mobileSheet === null ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
@@ -1167,6 +1189,7 @@ export function MapWorkbenchPage({
|
||||
events={operationalEvents}
|
||||
mode="expanded"
|
||||
selectedEventId={workspaceContext.operational.selectedEventId}
|
||||
onModeChange={closeMobileSheet}
|
||||
onSelectEvent={handleSelectOperationalEvent}
|
||||
onClearSelection={handleClearOperationalSelection}
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
|
||||
@@ -113,6 +113,65 @@ test("mobile launchers and map dock keep non-overlapping touch targets", async (
|
||||
expect(rectangles[1]!.x + rectangles[1]!.width).toBeLessThan(rectangles[2]!.x);
|
||||
});
|
||||
|
||||
test("mobile timeline only offers collapse after opening its detail sheet", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const timelineSummary = page.getByRole("region", { name: "移动端运行时间轴摘要" });
|
||||
await expect(timelineSummary).toBeVisible();
|
||||
await expect(timelineSummary.getByRole("button", { name: "隐藏运行时间轴" })).toHaveCount(0);
|
||||
await timelineSummary.getByRole("button", { name: "打开完整运行时间轴" }).click();
|
||||
|
||||
const timelineSheet = page.getByRole("region", { name: "运行时间轴抽屉" });
|
||||
await expect(timelineSheet).toBeVisible();
|
||||
await timelineSheet.getByRole("button", { name: "收起时间轴" }).click();
|
||||
await expect(timelineSheet).toBeHidden();
|
||||
await expect(timelineSummary).toBeVisible();
|
||||
});
|
||||
|
||||
test("mobile map tool panel stays above the bottom dock", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByRole("button", { name: "更多:更多地图操作" }).click();
|
||||
|
||||
const toolPanel = page.getByRole("region", { name: "更多" });
|
||||
const mapDock = page.locator('nav[aria-label="地图工具"]:visible');
|
||||
await expect(toolPanel).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [panelBox, dockBox] = await Promise.all([
|
||||
toolPanel.boundingBox(),
|
||||
mapDock.boundingBox()
|
||||
]);
|
||||
if (!panelBox || !dockBox) return Number.POSITIVE_INFINITY;
|
||||
return Math.round(panelBox.y + panelBox.height - dockBox.y);
|
||||
})
|
||||
.toBeLessThanOrEqual(-8);
|
||||
});
|
||||
|
||||
test("mobile map zoom stays above the bottom dock", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const mapZoom = page.getByRole("group", { name: "地图缩放" });
|
||||
const mapDock = page.locator('nav[aria-label="地图工具"]:visible');
|
||||
await expect(mapZoom).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [zoomBox, dockBox] = await Promise.all([
|
||||
mapZoom.boundingBox(),
|
||||
mapDock.boundingBox()
|
||||
]);
|
||||
if (!zoomBox || !dockBox) return Number.POSITIVE_INFINITY;
|
||||
return Math.round(zoomBox.y + zoomBox.height - dockBox.y);
|
||||
})
|
||||
.toBeLessThanOrEqual(-8);
|
||||
|
||||
const zoomBox = await mapZoom.boundingBox();
|
||||
expect(Math.round(375 - zoomBox!.x - zoomBox!.width)).toBe(12);
|
||||
});
|
||||
|
||||
test("Agent content buttons do not gain a border while pressed", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
Reference in New Issue
Block a user