wms-react-components
SKILL.md
WMS Map React Components (WMS 地圖 React 元件)
Overview
@rytass/wms-map-react-components 提供互動式倉庫空間編輯元件,基於 ReactFlow (XYFlow),支援背景圖、矩形/路徑繪製、多層編輯和歷史管理。
Quick Start
安裝
npm install @rytass/wms-map-react-components @xyflow/react @mezzanine-ui/react
基本使用
import { WMSMapModal, ViewMode } from '@rytass/wms-map-react-components';
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>開啟地圖編輯器</button>
<WMSMapModal
open={isOpen}
onClose={() => setIsOpen(false)}
viewMode={ViewMode.EDIT}
onSave={(mapData) => {
console.log('Saved:', mapData);
// 提交到後端
}}
/>
</>
);
}
Core Components
WMSMapModal
主要的地圖編輯模態框:
interface WMSMapModalProps {
open: boolean;
onClose: () => void;
viewMode?: ViewMode; // EDIT | VIEW(預設 EDIT)
title?: string; // 模態框標題
colorPalette?: string[]; // 區域顏色選項
onNodeClick?: (info: WMSNodeClickInfo) => void;
onSave?: (mapData: Map) => void;
onBreadcrumbClick?: (warehouseId: string, index: number) => void;
onNameChange?: (name: string) => Promise<void>; // 名稱變更回呼
initialNodes?: FlowNode[];
initialEdges?: FlowEdge[];
debugMode?: boolean;
maxFileSizeKB?: number; // 背景圖大小限制(預設 30720 = 30MB)
warehouseIds?: string[]; // 倉儲 ID 列表(用於 breadcrumb)
// 必要回呼:處理圖片上傳
onUpload: (files: File[]) => Promise<string[]>; // 回傳上傳後的 filename 陣列
// 可選:取得完整圖片 URL
getFilenameFQDN?: (filename: string) => Promise<string> | string;
}
子元件
| 元件 | 說明 |
|---|---|
WMSMapHeader |
工具列與標題 |
WMSMapContent |
畫布內容與編輯控制 |
Breadcrumb |
倉儲層級導航 |
ContextMenu |
右鍵菜單 |
Toolbar |
編輯工具列 |
節點類型
| 節點 | 說明 |
|---|---|
ImageNode |
背景圖節點 |
RectangleNode |
矩形區域節點 |
PathNode |
路徑/多邊形節點 |
Enums
ViewMode
enum ViewMode {
EDIT = 'EDIT', // 編輯模式
VIEW = 'VIEW', // 檢視模式
}
EditMode
enum EditMode {
BACKGROUND = 'BACKGROUND', // 背景圖編輯
LAYER = 'LAYER', // 區域層編輯
}
DrawingMode
enum DrawingMode {
NONE = 'NONE', // 無繪製
RECTANGLE = 'RECTANGLE', // 矩形繪製
PEN = 'PEN', // 自由繪製/路徑
}
LayerDrawingTool
enum LayerDrawingTool {
SELECT = 'SELECT', // 選取工具
RECTANGLE = 'RECTANGLE', // 矩形工具
PEN = 'PEN', // 鋼筆工具
}
MapRangeType
enum MapRangeType {
RECTANGLE = 'RECTANGLE', // 矩形區域
POLYGON = 'POLYGON', // 多邊形區域
}
MapRangeColor
enum MapRangeColor {
RED = 'RED',
YELLOW = 'YELLOW',
GREEN = 'GREEN',
BLUE = 'BLUE',
BLACK = 'BLACK',
}
Data Structures
ID 型別別名
export type ID = string;
Map
interface Map {
id: ID;
backgrounds: MapBackground[];
ranges: MapRange[]; // MapRectangleRange | MapPolygonRange
}
interface MapBackground {
id: string;
filename: string; // 檔案名稱(非完整 URL)
width: number;
height: number;
x: number;
y: number;
}
interface MapRectangleRange {
type: MapRangeType.RECTANGLE; // 'RECTANGLE'
id: string;
x: number;
y: number;
width: number;
height: number;
color: string;
}
interface MapPolygonRange {
type: MapRangeType.POLYGON; // 'POLYGON'
id: string;
points: MapPolygonRangePoint[];
color: string;
}
interface MapPolygonRangePoint {
x: number;
y: number;
}
WMSNodeClickInfo
type WMSNodeClickInfo =
| ImageNodeClickInfo
| RectangleNodeClickInfo
| PathNodeClickInfo;
// 基礎介面
interface NodeClickInfo {
id: string;
type: string;
position: { x: number; y: number };
zIndex?: number;
selected?: boolean;
}
interface ImageNodeClickInfo extends NodeClickInfo {
type: 'imageNode';
imageData: {
filename: string;
size: { width: number; height: number };
originalSize: { width: number; height: number };
imageUrl: string;
};
mapBackground: MapBackground; // 可直接傳給後端的格式
}
interface RectangleNodeClickInfo extends NodeClickInfo {
type: 'rectangleNode';
rectangleData: {
color: string;
size: { width: number; height: number };
};
mapRectangleRange: MapRectangleRange; // 可直接傳給後端的格式
}
interface PathNodeClickInfo extends NodeClickInfo {
type: 'pathNode';
pathData: {
color: string;
strokeWidth: number;
pointCount: number;
points: { x: number; y: number }[];
bounds: {
minX: number;
minY: number;
maxX: number;
maxY: number;
} | null;
};
mapPolygonRange: MapPolygonRange; // 可直接傳給後端的格式
}
Utility Functions
資料轉換
import {
transformNodeToClickInfo,
transformNodesToMapData,
transformApiDataToNodes,
validateMapData,
loadMapDataFromApi,
calculatePolygonBounds,
calculateNodeZIndex,
logMapData,
logNodeData,
} from '@rytass/wms-map-react-components';
// ReactFlow 節點 → 點擊資訊
const clickInfo = transformNodeToClickInfo(node);
// ReactFlow 節點 → 地圖資料 (後端格式)
const mapData = transformNodesToMapData(nodes);
// API 資料 → ReactFlow 節點(支援自訂圖片 URL 產生器)
const nodes = transformApiDataToNodes(apiData);
const nodesWithCustomUrl = transformApiDataToNodes(apiData, (filename) => {
return `https://cdn.example.com/images/${filename}`;
});
// 驗證地圖資料格式
const isValid = validateMapData(mapData);
// 輔助函數:計算多邊形邊界
const bounds = calculatePolygonBounds(points);
// 回傳: { minX, maxX, minY, maxY, width, height }
// 輔助函數:計算節點 zIndex
const zIndex = calculateNodeZIndex(existingNodes, 'rectangleNode');
// Debug 工具:格式化輸出資料到 console
logMapData(mapData); // 輸出完整地圖資料
logNodeData(node); // 輸出單一節點詳細資訊
Complete Example
import { useState, useCallback } from 'react';
import {
WMSMapModal,
ViewMode,
transformNodesToMapData,
transformApiDataToNodes,
WMSNodeClickInfo,
Map,
} from '@rytass/wms-map-react-components';
function WarehouseMapEditor() {
const [isOpen, setIsOpen] = useState(false);
const [mapData, setMapData] = useState<Map | null>(null);
// 從後端載入地圖資料
const loadMapData = async () => {
const response = await fetch('/api/warehouse/map');
const data = await response.json();
setMapData(data);
};
// 上傳圖片到後端(必要回呼)
const handleUpload = useCallback(async (files: File[]): Promise<string[]> => {
const formData = new FormData();
files.forEach((file) => formData.append('files', file));
const response = await fetch('/api/warehouse/upload', {
method: 'POST',
body: formData,
});
const { filenames } = await response.json();
return filenames; // 回傳 filename 陣列
}, []);
// 取得圖片完整 URL(可選)
const getFilenameFQDN = useCallback((filename: string) => {
return `https://cdn.example.com/warehouse-images/${filename}`;
}, []);
// 儲存地圖資料到後端
const handleSave = useCallback(async (data: Map) => {
await fetch('/api/warehouse/map', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
setMapData(data);
setIsOpen(false);
}, []);
// 處理區域點擊
const handleNodeClick = useCallback((info: WMSNodeClickInfo) => {
if (info.type === 'rectangleNode') {
// 取得後端格式的資料
console.log('Clicked zone:', info.mapRectangleRange);
// 可以導航到該區域的庫存頁面
}
}, []);
return (
<div>
<button onClick={() => { loadMapData(); setIsOpen(true); }}>
編輯倉儲地圖
</button>
<WMSMapModal
open={isOpen}
onClose={() => setIsOpen(false)}
viewMode={ViewMode.EDIT}
title="倉庫 A 地圖"
colorPalette={[
'#FF6B6B', // 紅 - 危險品區
'#4ECDC4', // 青 - 冷藏區
'#45B7D1', // 藍 - 一般存放區
'#96CEB4', // 綠 - 出貨區
'#FFEAA7', // 黃 - 暫存區
]}
initialNodes={mapData ? transformApiDataToNodes(mapData, getFilenameFQDN) : undefined}
onSave={handleSave}
onNodeClick={handleNodeClick}
onUpload={handleUpload}
getFilenameFQDN={getFilenameFQDN}
maxFileSizeKB={30720} // 30MB(預設值)
warehouseIds={['10001', '10001A']}
/>
</div>
);
}
// 唯讀檢視元件(VIEW 模式不需要 onUpload)
function WarehouseMapViewer({ mapData }: { mapData: Map }) {
const [selectedZone, setSelectedZone] = useState<string | null>(null);
const getFilenameFQDN = (filename: string) =>
`https://cdn.example.com/warehouse-images/${filename}`;
return (
<WMSMapModal
open={true}
onClose={() => {}}
viewMode={ViewMode.VIEW}
initialNodes={transformApiDataToNodes(mapData, getFilenameFQDN)}
onUpload={async () => []} // VIEW 模式下可用空實作
onNodeClick={(info) => {
if (info.type === 'rectangleNode') {
setSelectedZone(info.id);
}
}}
/>
);
}
多層級導航
function WarehouseNavigator() {
const [breadcrumb, setBreadcrumb] = useState([
{ id: 'warehouse-1', name: '主倉庫' },
]);
const handleBreadcrumbClick = (warehouseId: string, index: number) => {
// 導航到特定層級
setBreadcrumb(breadcrumb.slice(0, index + 1));
loadWarehouseMap(warehouseId);
};
const handleZoneClick = (info: WMSNodeClickInfo) => {
if (info.type === 'rectangleNode') {
// 進入子區域(需自行維護 zone 名稱對應)
setBreadcrumb([...breadcrumb, { id: info.id, name: `Zone ${info.id}` }]);
loadWarehouseMap(info.id);
}
};
return (
<WMSMapModal
open={true}
onClose={() => {}}
viewMode={ViewMode.VIEW}
onBreadcrumbClick={handleBreadcrumbClick}
onNodeClick={handleZoneClick}
/>
);
}
Constants
注意: Constants 目前未從主入口導出,若需使用請直接從 constants 路徑導入:
import { UI_CONFIG, CANVAS_CONFIG } from '@rytass/wms-map-react-components/constants';
// 預設尺寸
DEFAULT_IMAGE_WIDTH // 300
DEFAULT_IMAGE_HEIGHT // 200
DEFAULT_RECTANGLE_WIDTH // 150
DEFAULT_RECTANGLE_HEIGHT // 100
// 顏色
DEFAULT_RECTANGLE_COLOR // '#3b82f6'
SELECTION_BORDER_COLOR // '#3b82f6'
DEFAULT_BACKGROUND_TOOL_COLOR // '#3b82f6'
// 最小尺寸
MIN_RECTANGLE_SIZE // 10
MIN_RESIZE_WIDTH // 50
MIN_RESIZE_HEIGHT // 30
// 文字標籤
DEFAULT_RECTANGLE_LABEL // '矩形區域'
DEFAULT_PATH_LABEL // '路徑區域'
// 透明度
ACTIVE_OPACITY // 1
INACTIVE_OPACITY // 0.4
RECTANGLE_INACTIVE_OPACITY // 0.6
// 調整大小控制項尺寸
RESIZE_CONTROL_SIZE // 16
IMAGE_RESIZE_CONTROL_SIZE // 20
// UI 配置
UI_CONFIG.HISTORY_SIZE // 50 (Undo/Redo 歷史大小)
UI_CONFIG.COLOR_CHANGE_DELAY // 800ms
UI_CONFIG.STAGGER_DELAY // 100ms (圖片上傳間隔)
UI_CONFIG.NODE_SAVE_DELAY // 50ms
// Canvas 配置
CANVAS_CONFIG.MIN_ZOOM // 0.1
CANVAS_CONFIG.MAX_ZOOM // 4
CANVAS_CONFIG.DEFAULT_VIEWPORT // { x: 0, y: 0, zoom: 1 }
CANVAS_CONFIG.BACKGROUND_COLOR // '#F5F5F5'
// 檔案上傳配置
UPLOAD_CONFIG.DEFAULT_MAX_FILE_SIZE_KB // 30720 (30MB)
UPLOAD_CONFIG.SUPPORTED_MIME_TYPES // ['image/png', 'image/jpeg', ...]
// 支援的圖片類型
SUPPORTED_IMAGE_TYPES.ACCEPT // 'image/png,image/jpeg,image/jpg'
SUPPORTED_IMAGE_TYPES.PATTERN // /^image\/(png|jpeg|jpg)$/
SUPPORTED_IMAGE_TYPES.EXTENSIONS // ['png', 'jpg', 'jpeg']
// 預設倉庫 ID
DEFAULT_WAREHOUSE_IDS // ['10001', '10001A', '10002', '100002B', '100003', '100003B']
Dependencies
Required:
@xyflow/react^12.10.0
Peer Dependencies:
@mezzanine-ui/react@mezzanine-ui/react-hook-form-v2react,react-domreact-hook-form
Troubleshooting
地圖無法顯示
確保 ReactFlow Provider 正確包裝:
// WMSMapModal 內部已包含 ReactFlowProvider
// 不需要額外包裝
背景圖上傳失敗
檢查 maxFileSizeKB 設定,預設為 30720KB (30MB)。
節點無法拖曳
確認 viewMode 設為 ViewMode.EDIT。
React Hooks
@rytass/wms-map-react-components 提供以下內部 Hooks,用於地圖編輯器的核心功能。
注意: 這些 Hooks 目前為內部使用,未在主入口匯出。若需使用,請直接從相應檔案導入:
import { useContextMenu } from '@rytass/wms-map-react-components/hooks/use-context-menu';
useContextMenu
管理節點的右鍵菜單與圖層排列操作。
interface UseContextMenuProps {
id: string; // 節點 ID
editMode: EditMode; // 當前編輯模式
isEditable: boolean; // 是否可編輯
nodeType?: 'rectangleNode' | 'pathNode' | 'imageNode';
}
interface UseContextMenuReturn {
contextMenu: { visible: boolean; x: number; y: number };
handleContextMenu: (event: React.MouseEvent) => void;
handleCloseContextMenu: () => void;
handleDelete: () => void;
arrangeActions: {
onBringToFront: () => void; // 移至最上層
onBringForward: () => void; // 上移一層
onSendBackward: () => void; // 下移一層
onSendToBack: () => void; // 移至最下層
};
arrangeStates: {
canBringToFront: boolean;
canBringForward: boolean;
canSendBackward: boolean;
canSendToBack: boolean;
};
getNodes: () => Node[];
setNodes: (nodes: Node[] | ((nodes: Node[]) => Node[])) => void;
}
const { contextMenu, handleContextMenu, arrangeActions } = useContextMenu({
id: nodeId,
editMode: EditMode.LAYER,
isEditable: true,
nodeType: 'rectangleNode',
});
useDirectStateHistory
直接狀態歷史系統,支援 Undo/Redo 功能。
interface UseDirectStateHistoryOptions {
maxHistorySize?: number; // 預設 50
debugMode?: boolean; // 預設 false
}
interface UseDirectStateHistoryReturn {
saveState: (nodes: FlowNode[], edges: FlowEdge[], operation: string, editMode: EditMode) => void;
undo: () => { nodes: FlowNode[]; edges: FlowEdge[]; editMode: EditMode } | null;
redo: () => { nodes: FlowNode[]; edges: FlowEdge[]; editMode: EditMode } | null;
canUndo: boolean;
canRedo: boolean;
initializeHistory: (initialNodes: FlowNode[], initialEdges: FlowEdge[], editMode: EditMode) => void;
clearHistory: () => void;
getHistorySummary: () => HistorySummary;
history?: HistoryState[]; // debugMode 啟用時可用
currentIndex?: number; // debugMode 啟用時可用
}
const {
saveState,
undo,
redo,
canUndo,
canRedo,
initializeHistory,
} = useDirectStateHistory({ maxHistorySize: 100, debugMode: false });
// 初始化
initializeHistory(nodes, edges, EditMode.LAYER);
// 保存操作後狀態
saveState(nodes, edges, 'add-rectangle', EditMode.LAYER);
// 執行 Undo
const prevState = undo();
if (prevState) {
setNodes(prevState.nodes);
setEdges(prevState.edges);
}
usePenDrawing
鋼筆工具繪製多邊形/路徑區域。
interface UsePenDrawingProps {
editMode: EditMode;
drawingMode: DrawingMode;
onCreatePath: (points: { x: number; y: number }[]) => void;
}
interface UsePenDrawingReturn {
containerRef: React.RefObject<HTMLDivElement | null>; // 綁定到容器
isDrawing: boolean; // 是否正在繪製
previewPath: { x: number; y: number }[] | null; // 預覽路徑
currentPoints: { x: number; y: number }[]; // 已繪製的點
firstPoint: { x: number; y: number } | null; // 第一個點(閉合提示用)
canClose: boolean; // 是否可閉合(>=3 點)
forceComplete: () => void; // 強制完成繪製
}
const {
containerRef,
isDrawing,
previewPath,
canClose,
} = usePenDrawing({
editMode: EditMode.LAYER,
drawingMode: DrawingMode.PEN,
onCreatePath: (points) => {
// 建立路徑節點
createPathNode(points);
},
});
// 操作方式:
// - 單擊:添加點
// - 雙擊:完成並閉合路徑
// - 點擊第一個點:閉合路徑
// - Enter:完成並閉合路徑
// - Escape:取消繪製
// - Shift + 點擊:約束至 45 度角
useRectangleDrawing
矩形區域繪製工具。
interface UseRectangleDrawingProps {
editMode: EditMode;
drawingMode: DrawingMode;
onCreateRectangle: (startX: number, startY: number, endX: number, endY: number) => void;
}
interface UseRectangleDrawingReturn {
containerRef: React.RefObject<HTMLDivElement | null>;
isDrawing: boolean;
previewRect: {
x: number;
y: number;
width: number;
height: number;
} | null;
}
const {
containerRef,
isDrawing,
previewRect,
} = useRectangleDrawing({
editMode: EditMode.LAYER,
drawingMode: DrawingMode.RECTANGLE,
onCreateRectangle: (x1, y1, x2, y2) => {
createRectangleNode(x1, y1, x2, y2);
},
});
// 操作方式:
// - 按住滑鼠拖曳建立矩形
// - 放開滑鼠完成建立
// - 最小尺寸限制:MIN_RECTANGLE_SIZE
useTextEditing
節點文字標籤編輯功能。
interface UseTextEditingProps {
id: string; // 節點 ID
label: string; // 當前標籤文字
isEditable: boolean; // 是否可編輯
onTextEditComplete?: (id: string, oldText: string, newText: string) => void;
}
interface UseTextEditingReturn {
isEditing: boolean;
editingText: string;
inputRef: React.RefObject<HTMLInputElement | null>;
setEditingText: React.Dispatch<React.SetStateAction<string>>;
handleDoubleClick: (event: React.MouseEvent) => void; // 雙擊開始編輯
handleKeyDown: (event: React.KeyboardEvent) => void; // 鍵盤事件處理
handleBlur: () => void; // 失焦保存
updateNodeData: (updates: Record<string, unknown>) => void;
}
const {
isEditing,
editingText,
inputRef,
setEditingText,
handleDoubleClick,
handleKeyDown,
handleBlur,
} = useTextEditing({
id: nodeId,
label: 'Zone A',
isEditable: true,
onTextEditComplete: (id, oldText, newText) => {
console.log(`節點 ${id} 標籤從 "${oldText}" 改為 "${newText}"`);
saveState(nodes, edges, 'edit-label', editMode);
},
});
// 操作方式:
// - 雙擊節點:開始編輯
// - Enter:確認保存
// - Escape:取消編輯
// - 點擊外部:自動保存
Hooks 使用注意事項
- 必須在 ReactFlowProvider 內使用:所有 hooks 依賴
useReactFlow - 內部使用為主:這些 hooks 主要供 WMS 元件內部使用
- 歷史記錄整合:繪製與編輯操作應搭配
useDirectStateHistory記錄 - 模式檢查:各 hooks 會檢查
editMode和drawingMode狀態
Weekly Installs
7
Repository
rytass/utilsGitHub Stars
6
First Seen
Feb 5, 2026
Security Audits
Installed on
opencode7
github-copilot7
amp7
codex7
kimi-cli7
gemini-cli7