feat: add wallnote

This commit is contained in:
abearxiong 2025-02-28 21:40:44 +08:00
parent ba5ca8063a
commit d2c13c43a8
5 changed files with 206 additions and 33 deletions

View File

@ -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<SelectState>((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<HTMLDivElement | null>(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,
};
};

View File

@ -25,13 +25,14 @@ import { message } from '@/modules/message';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { BlankNoteText } from './constants'; import { BlankNoteText } from './constants';
import { Toolbar } from './modules/toolbar/Toolbar'; import { Toolbar } from './modules/toolbar/Toolbar';
import { useUserWallStore } from './store/user-wall';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { SaveModal } from './modules/FormDialog'; import { SaveModal } from './modules/FormDialog';
import { useTabNode } from './hooks/tab-node'; import { useTabNode } from './hooks/tab-node';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { useListenPaster } from './hooks/listen-copy'; import { useListenPaster } from './hooks/listen-copy';
import { ContextMenu } from './modules/ContextMenu'; import { ContextMenu } from './modules/ContextMenu';
import { useSelect } from './hooks/use-select';
import clsx from 'clsx';
type NodeData = { type NodeData = {
id: string; id: string;
position: XYPosition; position: XYPosition;
@ -40,31 +41,51 @@ type NodeData = {
export function FlowContent() { export function FlowContent() {
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
const [nodes, setNodes, onNodesChange] = useNodesState<NodeData>([]); const [nodes, setNodes, onNodesChange] = useNodesState<NodeData>([]);
const wallStore = useWallStore( const wallStore = useWallStore(
useShallow((state) => { useShallow((state) => {
return { return {
nodes: state.nodes, nodes: state.nodes,
saveNodes: state.saveNodes, saveNodes: state.saveNodes,
checkAndOpen: state.checkAndOpen, 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 store = useStore((state) => state);
const [mount, setMount] = useState(false); const [mount, setMount] = useState(false);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
const _onNodesChange = useCallback((changes: NodeChange[]) => { const _onNodesChange = useCallback((changes: NodeChange[]) => {
const [change] = changes; const [change] = changes;
if (change.type === 'remove') { if (change.type === 'remove') {
wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id)); wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id));
} }
if (change.type === 'position' && change.dragging === false) { if (change.type === 'position' && change.dragging === false) {
// console.log('position changes', change);
getNewNodes(false); getNewNodes(false);
} }
onNodesChange(changes); 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(() => { useEffect(() => {
setNodes(wallStore.nodes); setNodes(wallStore.nodes);
setMount(true); setMount(true);
@ -77,7 +98,6 @@ export function FlowContent() {
}; };
const getNewNodes = (showMessage = true) => { const getNewNodes = (showMessage = true) => {
const nodes = reactFlowInstance.getNodes(); const nodes = reactFlowInstance.getNodes();
console.log('showMessage', showMessage);
wallStore.saveNodes(nodes, { showMessage: showMessage }); wallStore.saveNodes(nodes, { showMessage: showMessage });
}; };
useEffect(() => { useEffect(() => {
@ -121,32 +141,72 @@ export function FlowContent() {
const handleCloseContextMenu = () => { const handleCloseContextMenu = () => {
setContextMenu(null); setContextMenu(null);
}; };
return ( return (
<ReactFlow <>
nodes={nodes} <div style={{ width: '100%', height: '100%', position: 'fixed', top: 0, left: 0, zIndex: 100 }} ref={canvasRef!}>
// debug={DEV_SERVER} <ReactFlow
fitView nodes={nodes}
onNodesChange={_onNodesChange} // debug={DEV_SERVER}
onNodeDoubleClick={onNodeDoubleClick} fitView
onPaneClick={onCheckPanelDoubleClick} onNodesChange={_onNodesChange}
zoomOnScroll={true} onNodeDoubleClick={onNodeDoubleClick}
preventScrolling={!hasFoucedNode} onPaneClick={onCheckPanelDoubleClick}
onContextMenu={handleContextMenu} zoomOnScroll={true}
minZoom={0.05} preventScrolling={!hasFoucedNode}
maxZoom={20} onContextMenu={handleContextMenu}
nodeTypes={CustomNodeType}> minZoom={0.05}
<Controls /> maxZoom={20}
<MiniMap /> className='cursor-grab'
<Background gap={[14, 14]} size={2} color='#E4E5E7' /> style={{ pointerEvents: wallStore.mouseSelect ? 'auto' : 'none' }}
<Panel position='top-left'> nodeTypes={CustomNodeType}>
<Toolbar /> <Controls className='bottom-10!'>
</Panel> <button
<Panel> type='button'
<Drawer /> className={clsx('react-flow__controls-button react-flow__controls-fitview', {
<SaveModal /> 'text-gray-500!': !wallStore.mouseSelect,
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />} })}
</Panel> title='fit view'
</ReactFlow> aria-label='fit view'
onClick={() => {
wallStore.setMouseSelect(!wallStore.mouseSelect);
if (wallStore.mouseSelect) {
message.info('框选模式');
} else {
message.info('拖动模式');
}
}}>
<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' className='remixicon w-4 h-4'>
<path d='M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z'></path>
</svg>
</button>
</Controls>
<MiniMap />
<Background gap={[14, 14]} size={2} color='#E4E5E7' />
<Panel position='top-left'>
<Toolbar />
</Panel>
<Panel>
<Drawer />
<SaveModal />
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
</Panel>
</ReactFlow>{' '}
{isSelecting && selectionBox && (
<div
style={{
position: 'absolute',
border: '1px dashed #000',
backgroundColor: 'rgba(0, 0, 255, 0.1)',
left: selectionBox.startX,
top: selectionBox.startY,
width: selectionBox.width,
height: selectionBox.height,
}}
/>
)}
</div>
</>
); );
} }
export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {

View File

@ -58,9 +58,7 @@ const Drawer = () => {
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
console.log('editValue', editValue, open, mounted);
if (!open && mounted) { if (!open && mounted) {
console.log('hasEdited', hasEdited);
if (hasEdited) { if (hasEdited) {
onSave(); onSave();
} }

View File

@ -55,6 +55,8 @@ interface WallState {
exportWall: (nodes: NodeData[]) => Promise<void>; exportWall: (nodes: NodeData[]) => Promise<void>;
clearQueryWall: () => Promise<void>; clearQueryWall: () => Promise<void>;
clearId: () => Promise<void>; clearId: () => Promise<void>;
mouseSelect: boolean;
setMouseSelect: (mouseSelect: boolean) => void;
} }
export const useWallStore = create<WallState>((set, get) => ({ export const useWallStore = create<WallState>((set, get) => ({
@ -65,7 +67,6 @@ export const useWallStore = create<WallState>((set, get) => ({
set({ nodes }); set({ nodes });
}, },
saveNodes: async (nodes: NodeData[], opts) => { saveNodes: async (nodes: NodeData[], opts) => {
console.log('nodes', nodes, opts, opts?.showMessage ?? true);
const showMessage = opts?.showMessage ?? true; const showMessage = opts?.showMessage ?? true;
set({ hasEdited: false }); set({ hasEdited: false });
if (!get().id) { if (!get().id) {
@ -75,7 +76,6 @@ export const useWallStore = create<WallState>((set, get) => ({
} else { } else {
const { id } = get(); const { id } = get();
const userWallStore = useUserWallStore.getState(); const userWallStore = useUserWallStore.getState();
console.log('saveNodes id', id);
if (id) { if (id) {
const covertData = getNodeData(nodes); const covertData = getNodeData(nodes);
const res = await userWallStore.saveWall({ const res = await userWallStore.saveWall({
@ -199,4 +199,6 @@ export const useWallStore = create<WallState>((set, get) => ({
clearQueryWall: async () => { clearQueryWall: async () => {
set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false }); set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false });
}, },
mouseSelect: true,
setMouseSelect: (mouseSelect) => set({ mouseSelect }),
})); }));

View File

@ -0,0 +1,9 @@
/**
* html标签
* @param str
* @returns
*/
export function isHTML(str) {
// 使用正则表达式检查是否存在 HTML 标签
return /<[^>]+>/i.test(str);
}