Files
next-tjwater-drainage-frontend/features/workbench/components/toolbar-panel.tsx
T

441 lines
14 KiB
TypeScript

import {
Activity,
Camera,
Circle,
Copy,
Database,
Dot,
Download,
Keyboard,
Layers3,
MapPinned,
Pentagon,
Redo2,
RefreshCw,
Route,
Ruler,
Share2,
Trash2,
Undo2,
Waypoints,
type LucideIcon
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import {
MapAnnotationPanel,
type MapAnnotationItem,
type MapAnnotationShareAction
} from "@/features/map/core/components/annotation-panel";
import type { BaseLayerOption } from "@/features/map/core/components/base-layers-control";
import {
MapActionRow,
MapControlPanel,
MapMetricTile,
MapPanelSection
} from "@/features/map/core/components/control-panel";
import { MapDrawToolbar } from "@/features/map/core/components/draw-toolbar";
import { MapLayerPanel } from "@/features/map/core/components/layer-panel";
import type { MapLayerControlItem } from "@/features/map/core/components/layer-control";
import { MapLegendList, type MapLegendItem } from "@/features/map/core/components/legend";
import { MapMeasurePanel } from "@/features/map/core/components/measure-panel";
import {
type WorkbenchDrawingAnnotation,
type WorkbenchDrawMode
} from "../hooks/use-workbench-drawing";
import {
type WorkbenchMeasurementResult,
type WorkbenchMeasureMode,
type WorkbenchMeasureUnit
} from "../hooks/use-workbench-measurement";
import { waterNetworkToolbarItems } from "../map/toolbar-config";
export type ToolbarToolId = "layers" | "measure" | "analysis" | "annotation" | "more";
export type ExportViewPreset = "current" | "4k";
type MeasureModeOption = {
id: WorkbenchMeasureMode;
label: string;
icon: LucideIcon;
};
const MEASURE_MODES: MeasureModeOption[] = [
{ id: "distance", label: "距离", icon: Route },
{ id: "area", label: "面积", icon: Pentagon },
{ id: "segment", label: "管段", icon: Waypoints }
];
const MEASURE_UNITS: Array<{ id: WorkbenchMeasureUnit; label: string }> = [
{ id: "m", label: "m" },
{ id: "km", label: "km" }
];
const ANNOTATION_SHARE_ACTIONS: MapAnnotationShareAction[] = [
{
id: "export",
icon: Share2,
label: "导出标注",
description: "GeoJSON / 截图报告"
}
];
const TOOL_PANEL_EXIT_MS = 160;
const DRAW_TOOL_OPTIONS: Array<{
id: WorkbenchDrawMode | "undo" | "redo" | "delete" | "clear";
label: string;
icon: LucideIcon;
tone?: "danger";
}> = [
{ id: "point", label: "点标注", icon: Dot },
{ id: "line", label: "线标注", icon: Route },
{ id: "polygon", label: "范围标注", icon: Pentagon },
{ id: "circle", label: "圆形标注", icon: Circle },
{ id: "undo", label: "撤销", icon: Undo2 },
{ id: "redo", label: "重做", icon: Redo2 },
{ id: "delete", label: "删除选中", icon: Trash2, tone: "danger" },
{ id: "clear", label: "清空全部", icon: Trash2, tone: "danger" }
];
export type ToolbarPanelProps = {
activeToolId: ToolbarToolId | null;
activeMeasureModeId: WorkbenchMeasureMode;
activeMeasureUnitId: WorkbenchMeasureUnit;
measuring: boolean;
measureResult: WorkbenchMeasurementResult;
measureEnabled: boolean;
hasMeasurePoints: boolean;
activeDrawToolId: WorkbenchDrawMode | null;
drawingEnabled: boolean;
drawingAnnotations: WorkbenchDrawingAnnotation[];
canUndoDrawing: boolean;
canRedoDrawing: boolean;
canDeleteSelectedDrawing: boolean;
hasDrawingFeatures: boolean;
layerItems: MapLayerControlItem[];
legendItems: MapLegendItem[];
baseLayerOptions: BaseLayerOption[];
activeBaseLayerId: string;
onSelectMeasureMode: (id: WorkbenchMeasureMode) => void;
onSelectMeasureUnit: (id: WorkbenchMeasureUnit) => void;
onStartMeasure: () => void;
onStopMeasure: () => void;
onClearMeasure: () => void;
onCopyMeasure: () => void;
onSelectDrawTool: (id: WorkbenchDrawMode | null) => void;
onUndoDrawing: () => void;
onRedoDrawing: () => void;
onDeleteSelectedDrawing: () => void;
onClearDrawing: () => void;
onExportAnnotations: () => void;
onSelectBaseLayer: (id: string) => void;
onToggleLayer: (id: string, visible: boolean) => void;
onExportView: (preset: ExportViewPreset) => void;
onRefreshTiles: () => void;
onShowDataStatus: () => void;
onShowShortcuts: () => void;
onExportConfig: () => void;
panelWidthClassName?: string;
};
export function ToolbarPanel({
activeToolId,
activeMeasureModeId,
activeMeasureUnitId,
measuring,
measureResult,
measureEnabled,
hasMeasurePoints,
activeDrawToolId,
drawingEnabled,
drawingAnnotations,
canUndoDrawing,
canRedoDrawing,
canDeleteSelectedDrawing,
hasDrawingFeatures,
layerItems,
legendItems,
baseLayerOptions,
activeBaseLayerId,
onSelectMeasureMode,
onSelectMeasureUnit,
onStartMeasure,
onStopMeasure,
onClearMeasure,
onCopyMeasure,
onSelectDrawTool,
onUndoDrawing,
onRedoDrawing,
onDeleteSelectedDrawing,
onClearDrawing,
onExportAnnotations,
onSelectBaseLayer,
onToggleLayer,
onExportView,
onRefreshTiles,
onShowDataStatus,
onShowShortcuts,
onExportConfig,
panelWidthClassName
}: ToolbarPanelProps) {
const annotationItems = useMemo<MapAnnotationItem[]>(
() =>
drawingAnnotations.map((annotation) => ({
id: annotation.id,
icon: getDrawingAnnotationIcon(annotation.kind),
label: annotation.label,
description: annotation.description,
status: annotation.hidden ? "隐藏" : "显示",
selected: annotation.selected,
muted: annotation.hidden,
onClick: annotation.onToggleVisibility
})),
[drawingAnnotations]
);
const annotationShareActions = useMemo(
() =>
ANNOTATION_SHARE_ACTIONS.map((action) => ({
...action,
onClick: action.id === "export" ? onExportAnnotations : action.onClick
})),
[onExportAnnotations]
);
const [renderedToolId, setRenderedToolId] = useState<ToolbarToolId | null>(activeToolId);
const panelVisible = Boolean(activeToolId);
useEffect(() => {
if (activeToolId) {
setRenderedToolId(activeToolId);
return;
}
if (!renderedToolId) {
return;
}
const exitTimer = window.setTimeout(() => {
setRenderedToolId(null);
}, TOOL_PANEL_EXIT_MS);
return () => window.clearTimeout(exitTimer);
}, [activeToolId, renderedToolId]);
if (!renderedToolId) {
return null;
}
const activeTool = waterNetworkToolbarItems.find((item) => item.id === renderedToolId);
const Icon = activeTool?.icon ?? Layers3;
if (renderedToolId === "layers") {
return (
<MapControlPanel
key="layers"
title="图层"
description={`${layerItems.filter((item) => item.visible).length}/${layerItems.length} 个业务图层可见`}
icon={Icon}
widthClassName={panelWidthClassName}
visible={panelVisible}
>
<MapLayerPanel
layerItems={layerItems}
baseLayerOptions={baseLayerOptions}
activeBaseLayerId={activeBaseLayerId}
onSelectBaseLayer={onSelectBaseLayer}
onToggleLayer={onToggleLayer}
/>
</MapControlPanel>
);
}
if (renderedToolId === "measure") {
return (
<MapControlPanel key="measure" title="测量" description="距离与范围测量" icon={Icon} widthClassName={panelWidthClassName ?? "w-[312px]"} visible={panelVisible}>
<MapMeasurePanel
modes={MEASURE_MODES}
activeModeId={activeMeasureModeId}
resultLabel={measureResult.label}
resultValue={measureResult.value}
resultUnit={measureResult.unit}
metrics={measureResult.metrics}
units={MEASURE_UNITS}
activeUnitId={activeMeasureUnitId}
actions={[
{
id: measuring ? "stop" : "start",
icon: Ruler,
label: measuring ? "停止测量" : "开始测量",
description:
activeMeasureModeId === "segment"
? measuring
? "暂停管线选择"
: "点击管线选择资产"
: measuring
? "暂停地图取点"
: "在地图上连续点击取点",
variant: measuring ? "default" : "primary",
disabled: !measureEnabled,
onClick: measuring ? onStopMeasure : onStartMeasure
},
{
id: "copy",
icon: Copy,
label: "复制结果",
description: "复制当前读数",
disabled: !hasMeasurePoints,
onClick: onCopyMeasure
},
{
id: "clear",
icon: Trash2,
label: "清除测量",
description: activeMeasureModeId === "segment" ? "清空已选管段" : "移除临时测量线",
tone: "danger",
disabled: !hasMeasurePoints,
onClick: onClearMeasure
}
]}
onSelectMode={(id) => onSelectMeasureMode(id as WorkbenchMeasureMode)}
onSelectUnit={(id) => onSelectMeasureUnit(id as WorkbenchMeasureUnit)}
/>
</MapControlPanel>
);
}
if (renderedToolId === "analysis") {
return (
<MapControlPanel key="analysis" title="分析" description="图例与当前覆盖" icon={Icon} widthClassName={panelWidthClassName ?? "w-[316px]"} visible={panelVisible}>
<AnalysisPanel legendItems={legendItems} />
</MapControlPanel>
);
}
if (renderedToolId === "annotation") {
return (
<MapControlPanel key="annotation" title="标注" description="创建与管理地图标注" icon={Icon} widthClassName={panelWidthClassName ?? "w-[312px]"} visible={panelVisible}>
<MapPanelSection title="绘制">
<MapDrawToolbar
variant="panel"
items={DRAW_TOOL_OPTIONS.map((item) => ({
...item,
active: activeDrawToolId === item.id,
disabled:
!drawingEnabled ||
(item.id === "undo" && !canUndoDrawing) ||
(item.id === "redo" && !canRedoDrawing) ||
(item.id === "delete" && !canDeleteSelectedDrawing) ||
(item.id === "clear" && !canUndoDrawing && !hasDrawingFeatures),
onClick: () => {
if (item.id === "undo") {
onUndoDrawing();
return;
}
if (item.id === "redo") {
onRedoDrawing();
return;
}
if (item.id === "delete") {
onDeleteSelectedDrawing();
return;
}
if (item.id === "clear") {
onClearDrawing();
return;
}
onSelectDrawTool(activeDrawToolId === item.id ? null : item.id);
}
}))}
/>
</MapPanelSection>
<MapAnnotationPanel annotations={annotationItems} shareActions={annotationShareActions} />
</MapControlPanel>
);
}
return (
<MapControlPanel key="more" title="更多" description={activeTool?.description} icon={Icon} widthClassName={panelWidthClassName ?? "w-[292px]"} visible={panelVisible}>
<MorePanel
onExportView={onExportView}
onRefreshTiles={onRefreshTiles}
onShowDataStatus={onShowDataStatus}
onShowShortcuts={onShowShortcuts}
onExportConfig={onExportConfig}
/>
</MapControlPanel>
);
}
function AnalysisPanel({ legendItems }: { legendItems: MapLegendItem[] }) {
return (
<div className="space-y-3">
<MapPanelSection title="图例">
<MapLegendList items={legendItems} showStatusDot />
</MapPanelSection>
<MapPanelSection title="当前覆盖">
<div className="grid grid-cols-2 gap-2">
<MapMetricTile label="影响范围" value="未开启" />
<MapMetricTile label="风险预览" value="待生成" />
</div>
<MapActionRow icon={Activity} label="模拟结果" description="影响面、事故点与标签" />
<MapActionRow icon={MapPinned} label="选中对象" description="点击管线或节点查看详情" />
</MapPanelSection>
</div>
);
}
type MorePanelProps = {
onExportView: (preset: ExportViewPreset) => void;
onRefreshTiles: () => void;
onShowDataStatus: () => void;
onShowShortcuts: () => void;
onExportConfig: () => void;
};
function MorePanel({
onExportView,
onRefreshTiles,
onShowDataStatus,
onShowShortcuts,
onExportConfig
}: MorePanelProps) {
return (
<div className="space-y-3">
<MapPanelSection title="导出视图">
<MapActionRow icon={Camera} label="当前分辨率" description="按当前地图画布尺寸导出" onClick={() => onExportView("current")} />
<MapActionRow icon={Camera} label="4K 分辨率" description="长边约 3840px 的 PNG" onClick={() => onExportView("4k")} />
</MapPanelSection>
<MapPanelSection title="数据">
<MapActionRow icon={RefreshCw} label="刷新瓦片" description="请求地图重新渲染" onClick={onRefreshTiles} />
<MapActionRow icon={Database} label="数据状态" description="查看当前数据源状态" status="查看" onClick={onShowDataStatus} />
</MapPanelSection>
<MapPanelSection title="系统">
<MapActionRow icon={Keyboard} label="快捷键" description="查看地图操作键位" onClick={onShowShortcuts} />
<MapActionRow icon={Download} label="导出配置" description="保存当前图层与工具状态" onClick={onExportConfig} />
</MapPanelSection>
</div>
);
}
function getDrawingAnnotationIcon(kind: WorkbenchDrawMode) {
if (kind === "line") {
return Route;
}
if (kind === "polygon") {
return Pentagon;
}
if (kind === "circle") {
return Circle;
}
return MapPinned;
}