From d2c13c43a8e6c3b05627cda60918b68c3de06f9c Mon Sep 17 00:00:00 2001 From: abearxiong Date: Fri, 28 Feb 2025 21:40:44 +0800 Subject: [PATCH] feat: add wallnote --- src/pages/wall/hooks/use-select.ts | 104 +++++++++++++++++++++++++ src/pages/wall/index.tsx | 118 ++++++++++++++++++++++------- src/pages/wall/modules/Drawer.tsx | 2 - src/pages/wall/store/wall.ts | 6 +- src/pages/wall/utils/html.ts | 9 +++ 5 files changed, 206 insertions(+), 33 deletions(-) create mode 100644 src/pages/wall/hooks/use-select.ts create mode 100644 src/pages/wall/utils/html.ts diff --git a/src/pages/wall/hooks/use-select.ts b/src/pages/wall/hooks/use-select.ts new file mode 100644 index 0000000..850b5cd --- /dev/null +++ b/src/pages/wall/hooks/use-select.ts @@ -0,0 +1,104 @@ +/** + * 实现当xyflow在空白处点击,并拖动,有一个矩形框,框选多个节点。 + * 1. 在xyflow的源码中找到矩形框选的实现,找到矩形框选的逻辑 + * 2. 在xyflow的源码中找到空白处点击的实现,找到空白处点击的逻辑 + * 3. 在xyflow的源码中找到拖动的实现,找到拖动的逻辑 + * 4. 将矩形框选的逻辑和空白处点击的逻辑和拖动的逻辑结合起来,实现矩形框选多个节点 + */ + +import { useState, useEffect, useRef, useMemo } from 'react'; +import { useReactFlow, useStoreApi } from '@xyflow/react'; +import { useWallStore } from '../store/wall'; +import { useShallow } from 'zustand/react/shallow'; + +import { create } from 'zustand'; +type SelectState = { + isSelecting: boolean; + selectionBox: { startX: number; startY: number; width: number; height: number } | null; + setIsSelecting: (isSelecting: boolean) => void; + setSelectionBox: (selectionBox: { startX: number; startY: number; width: number; height: number } | null) => void; +}; +export const useSelectStore = create((set) => ({ + isSelecting: false, + selectionBox: null, + setIsSelecting: (isSelecting: boolean) => set({ isSelecting }), + setSelectionBox: (selectionBox: { startX: number; startY: number; width: number; height: number } | null) => set({ selectionBox }), +})); +type UserSelectOpts = { + onSelect: (nodes: any[]) => void; + listenMouseDown?: boolean; +}; +export const useSelect = (opts: UserSelectOpts) => { + const { isSelecting, setIsSelecting, selectionBox, setSelectionBox } = useSelectStore(); + const canvasRef = useRef(null); + const reactFlowInstance = useReactFlow(); + const storeApi = useStoreApi(); + const wallStore = useWallStore( + useShallow((state) => { + return { + mouseSelect: !state.mouseSelect, + setMouseSelect: state.setMouseSelect, + }; + }), + ); + useEffect(() => { + const canvas = canvasRef.current!; + canvas.addEventListener('mousedown', handleMouseDown); + canvas.addEventListener('mousemove', handleMouseMove); + canvas.addEventListener('mouseup', handleMouseUp); + return () => { + canvas.removeEventListener('mousedown', handleMouseDown); + canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mouseup', handleMouseUp); + }; + }, [isSelecting, selectionBox, wallStore.mouseSelect]); + const handleMouseDown = (event) => { + if (event.target === canvasRef.current) { + setIsSelecting(true); + setSelectionBox({ startX: event.clientX, startY: event.clientY, width: 0, height: 0 }); + } + }; + + const handleMouseMove = (event) => { + if (isSelecting && selectionBox) { + const newWidth = event.clientX - selectionBox.startX; + const newHeight = event.clientY - selectionBox.startY; + setSelectionBox({ + startX: selectionBox.startX, + startY: selectionBox.startY, + width: newWidth, + height: newHeight, + }); + } + }; + + const handleMouseUp = () => { + if (isSelecting) { + const nodes = getNodesWithinSelectionBox(selectionBox); + opts.onSelect(nodes); + setIsSelecting(false); + setSelectionBox(null); + } + }; + const getNodesWithinSelectionBox = (box) => { + // Implement logic to find nodes within the selection box + // This will depend on how your nodes are structured and rendered + const nodes = storeApi.getState().nodes; + const screenPosition = reactFlowInstance.screenToFlowPosition({ + x: box.startX, + y: box.startY, + }); + const nodesWithinBox = nodes.filter((node) => { + const { x, y } = node.position; + return x >= screenPosition.x && x <= screenPosition.x + box.width && y >= screenPosition.y && y <= screenPosition.y + box.height; + }); + return nodesWithinBox; + }; + + return { + isSelecting, + setIsSelecting, + selectionBox, + canvasRef, + }; +}; diff --git a/src/pages/wall/index.tsx b/src/pages/wall/index.tsx index d156cb0..88ae93a 100644 --- a/src/pages/wall/index.tsx +++ b/src/pages/wall/index.tsx @@ -25,13 +25,14 @@ import { message } from '@/modules/message'; import { useShallow } from 'zustand/react/shallow'; import { BlankNoteText } from './constants'; import { Toolbar } from './modules/toolbar/Toolbar'; -import { useUserWallStore } from './store/user-wall'; import { useNavigate, useParams } from 'react-router-dom'; import { SaveModal } from './modules/FormDialog'; import { useTabNode } from './hooks/tab-node'; import { Button } from '@mui/material'; import { useListenPaster } from './hooks/listen-copy'; import { ContextMenu } from './modules/ContextMenu'; +import { useSelect } from './hooks/use-select'; +import clsx from 'clsx'; type NodeData = { id: string; position: XYPosition; @@ -40,31 +41,51 @@ type NodeData = { export function FlowContent() { const reactFlowInstance = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState([]); + const wallStore = useWallStore( useShallow((state) => { return { nodes: state.nodes, saveNodes: state.saveNodes, checkAndOpen: state.checkAndOpen, + mouseSelect: state.mouseSelect, // 鼠标模式,不能拖动 + setMouseSelect: state.setMouseSelect, }; }), ); + const { isSelecting, selectionBox, setIsSelecting, canvasRef } = useSelect({ + onSelect: (nodes) => { + setSelectedNodes(nodes); + }, + listenMouseDown: !wallStore.mouseSelect, + }); const store = useStore((state) => state); const [mount, setMount] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const _onNodesChange = useCallback((changes: NodeChange[]) => { const [change] = changes; - if (change.type === 'remove') { wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id)); } if (change.type === 'position' && change.dragging === false) { - // console.log('position changes', change); getNewNodes(false); } onNodesChange(changes); }, []); + const setSelectedNodes = (nodes: any[]) => { + const _nodes = reactFlowInstance.getNodes(); + const selectedNodes = nodes.map((node) => node.id); + const newNodes = _nodes.map((node) => { + if (selectedNodes.includes(node.id)) { + return { ...node, selected: true }; + } + delete node.selected; + return node; + }); + + reactFlowInstance.setNodes(newNodes); + }; useEffect(() => { setNodes(wallStore.nodes); setMount(true); @@ -77,7 +98,6 @@ export function FlowContent() { }; const getNewNodes = (showMessage = true) => { const nodes = reactFlowInstance.getNodes(); - console.log('showMessage', showMessage); wallStore.saveNodes(nodes, { showMessage: showMessage }); }; useEffect(() => { @@ -121,32 +141,72 @@ export function FlowContent() { const handleCloseContextMenu = () => { setContextMenu(null); }; + return ( - - - - - - - - - - - {contextMenu && } - - + <> +
+ + + + + + + + + + + + + {contextMenu && } + + {' '} + {isSelecting && selectionBox && ( +
+ )} +
+ ); } export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { diff --git a/src/pages/wall/modules/Drawer.tsx b/src/pages/wall/modules/Drawer.tsx index a537a71..0e8df63 100644 --- a/src/pages/wall/modules/Drawer.tsx +++ b/src/pages/wall/modules/Drawer.tsx @@ -58,9 +58,7 @@ const Drawer = () => { }; }, []); useEffect(() => { - console.log('editValue', editValue, open, mounted); if (!open && mounted) { - console.log('hasEdited', hasEdited); if (hasEdited) { onSave(); } diff --git a/src/pages/wall/store/wall.ts b/src/pages/wall/store/wall.ts index e77b749..d6feb00 100644 --- a/src/pages/wall/store/wall.ts +++ b/src/pages/wall/store/wall.ts @@ -55,6 +55,8 @@ interface WallState { exportWall: (nodes: NodeData[]) => Promise; clearQueryWall: () => Promise; clearId: () => Promise; + mouseSelect: boolean; + setMouseSelect: (mouseSelect: boolean) => void; } export const useWallStore = create((set, get) => ({ @@ -65,7 +67,6 @@ export const useWallStore = create((set, get) => ({ set({ nodes }); }, saveNodes: async (nodes: NodeData[], opts) => { - console.log('nodes', nodes, opts, opts?.showMessage ?? true); const showMessage = opts?.showMessage ?? true; set({ hasEdited: false }); if (!get().id) { @@ -75,7 +76,6 @@ export const useWallStore = create((set, get) => ({ } else { const { id } = get(); const userWallStore = useUserWallStore.getState(); - console.log('saveNodes id', id); if (id) { const covertData = getNodeData(nodes); const res = await userWallStore.saveWall({ @@ -199,4 +199,6 @@ export const useWallStore = create((set, get) => ({ clearQueryWall: async () => { set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false }); }, + mouseSelect: true, + setMouseSelect: (mouseSelect) => set({ mouseSelect }), })); diff --git a/src/pages/wall/utils/html.ts b/src/pages/wall/utils/html.ts new file mode 100644 index 0000000..d5f487b --- /dev/null +++ b/src/pages/wall/utils/html.ts @@ -0,0 +1,9 @@ +/** + * 存在html标签 + * @param str + * @returns + */ +export function isHTML(str) { + // 使用正则表达式检查是否存在 HTML 标签 + return /<[^>]+>/i.test(str); +}