=> {
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 (
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']}
/>
);
}
// 唯讀檢視元件(VIEW 模式不需要 onUpload)
function WarehouseMapViewer({ mapData }: { mapData: Map }) {
const [selectedZone, setSelectedZone] = useState(null);
const getFilenameFQDN = (filename: string) =>
`https://cdn.example.com/warehouse-images/${filename}`;
return (
{}}
viewMode={ViewMode.VIEW}
initialNodes={transformApiDataToNodes(mapData, getFilenameFQDN)}
onUpload={async () => []} // VIEW 模式下可用空實作
onNodeClick={(info) => {
if (info.type === 'rectangleNode') {
setSelectedZone(info.id);
}
}}
/>
);
}
```
## 多層級導航
```tsx
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 (
{}}
viewMode={ViewMode.VIEW}
onBreadcrumbClick={handleBreadcrumbClick}
onNodeClick={handleZoneClick}
/>
);
}
```
## Constants
> **注意:** Constants 目前未從主入口導出,若需使用請直接從 constants 路徑導入:
> ```typescript
> import { UI_CONFIG, CANVAS_CONFIG } from '@rytass/wms-map-react-components/constants';
> ```
```typescript
// 預設尺寸
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-v2`
- `react`, `react-dom`
- `react-hook-form`
## Troubleshooting
### 地圖無法顯示
確保 ReactFlow Provider 正確包裝:
```tsx
// WMSMapModal 內部已包含 ReactFlowProvider
// 不需要額外包裝
```
### 背景圖上傳失敗
檢查 `maxFileSizeKB` 設定,預設為 30720KB (30MB)。
### 節點無法拖曳
確認 `viewMode` 設為 `ViewMode.EDIT`。
## React Hooks
`@rytass/wms-map-react-components` 提供以下內部 Hooks,用於地圖編輯器的核心功能。
> **注意:** 這些 Hooks 目前為內部使用,未在主入口匯出。若需使用,請直接從相應檔案導入:
> ```typescript
> import { useContextMenu } from '@rytass/wms-map-react-components/hooks/use-context-menu';
> ```
### useContextMenu
管理節點的右鍵菜單與圖層排列操作。
```typescript
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 功能。
```typescript
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
鋼筆工具繪製多邊形/路徑區域。
```typescript
interface UsePenDrawingProps {
editMode: EditMode;
drawingMode: DrawingMode;
onCreatePath: (points: { x: number; y: number }[]) => void;
}
interface UsePenDrawingReturn {
containerRef: React.RefObject; // 綁定到容器
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
矩形區域繪製工具。
```typescript
interface UseRectangleDrawingProps {
editMode: EditMode;
drawingMode: DrawingMode;
onCreateRectangle: (startX: number, startY: number, endX: number, endY: number) => void;
}
interface UseRectangleDrawingReturn {
containerRef: React.RefObject;
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
節點文字標籤編輯功能。
```typescript
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;
setEditingText: React.Dispatch>;
handleDoubleClick: (event: React.MouseEvent) => void; // 雙擊開始編輯
handleKeyDown: (event: React.KeyboardEvent) => void; // 鍵盤事件處理
handleBlur: () => void; // 失焦保存
updateNodeData: (updates: Record) => 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 使用注意事項
1. **必須在 ReactFlowProvider 內使用**:所有 hooks 依賴 `useReactFlow`
2. **內部使用為主**:這些 hooks 主要供 WMS 元件內部使用
3. **歷史記錄整合**:繪製與編輯操作應搭配 `useDirectStateHistory` 記錄
4. **模式檢查**:各 hooks 會檢查 `editMode` 和 `drawingMode` 狀態