generated from template/vite-react-template
feat: add wallnote
This commit is contained in:
parent
ba5ca8063a
commit
d2c13c43a8
104
src/pages/wall/hooks/use-select.ts
Normal file
104
src/pages/wall/hooks/use-select.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -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,7 +141,10 @@ export function FlowContent() {
|
|||||||
const handleCloseContextMenu = () => {
|
const handleCloseContextMenu = () => {
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ width: '100%', height: '100%', position: 'fixed', top: 0, left: 0, zIndex: 100 }} ref={canvasRef!}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
// debug={DEV_SERVER}
|
// debug={DEV_SERVER}
|
||||||
@ -134,8 +157,30 @@ export function FlowContent() {
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
minZoom={0.05}
|
minZoom={0.05}
|
||||||
maxZoom={20}
|
maxZoom={20}
|
||||||
|
className='cursor-grab'
|
||||||
|
style={{ pointerEvents: wallStore.mouseSelect ? 'auto' : 'none' }}
|
||||||
nodeTypes={CustomNodeType}>
|
nodeTypes={CustomNodeType}>
|
||||||
<Controls />
|
<Controls className='bottom-10!'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={clsx('react-flow__controls-button react-flow__controls-fitview', {
|
||||||
|
'text-gray-500!': !wallStore.mouseSelect,
|
||||||
|
})}
|
||||||
|
title='fit view'
|
||||||
|
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 />
|
<MiniMap />
|
||||||
<Background gap={[14, 14]} size={2} color='#E4E5E7' />
|
<Background gap={[14, 14]} size={2} color='#E4E5E7' />
|
||||||
<Panel position='top-left'>
|
<Panel position='top-left'>
|
||||||
@ -146,7 +191,22 @@ export function FlowContent() {
|
|||||||
<SaveModal />
|
<SaveModal />
|
||||||
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</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 }) => {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
9
src/pages/wall/utils/html.ts
Normal file
9
src/pages/wall/utils/html.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 存在html标签
|
||||||
|
* @param str
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function isHTML(str) {
|
||||||
|
// 使用正则表达式检查是否存在 HTML 标签
|
||||||
|
return /<[^>]+>/i.test(str);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user