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 { 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<NodeData>([]);
|
||||
|
||||
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 (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
// debug={DEV_SERVER}
|
||||
fitView
|
||||
onNodesChange={_onNodesChange}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onPaneClick={onCheckPanelDoubleClick}
|
||||
zoomOnScroll={true}
|
||||
preventScrolling={!hasFoucedNode}
|
||||
onContextMenu={handleContextMenu}
|
||||
minZoom={0.05}
|
||||
maxZoom={20}
|
||||
nodeTypes={CustomNodeType}>
|
||||
<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>
|
||||
<>
|
||||
<div style={{ width: '100%', height: '100%', position: 'fixed', top: 0, left: 0, zIndex: 100 }} ref={canvasRef!}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
// debug={DEV_SERVER}
|
||||
fitView
|
||||
onNodesChange={_onNodesChange}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onPaneClick={onCheckPanelDoubleClick}
|
||||
zoomOnScroll={true}
|
||||
preventScrolling={!hasFoucedNode}
|
||||
onContextMenu={handleContextMenu}
|
||||
minZoom={0.05}
|
||||
maxZoom={20}
|
||||
className='cursor-grab'
|
||||
style={{ pointerEvents: wallStore.mouseSelect ? 'auto' : 'none' }}
|
||||
nodeTypes={CustomNodeType}>
|
||||
<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 />
|
||||
<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 }) => {
|
||||
|
@ -58,9 +58,7 @@ const Drawer = () => {
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
console.log('editValue', editValue, open, mounted);
|
||||
if (!open && mounted) {
|
||||
console.log('hasEdited', hasEdited);
|
||||
if (hasEdited) {
|
||||
onSave();
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ interface WallState {
|
||||
exportWall: (nodes: NodeData[]) => Promise<void>;
|
||||
clearQueryWall: () => Promise<void>;
|
||||
clearId: () => Promise<void>;
|
||||
mouseSelect: boolean;
|
||||
setMouseSelect: (mouseSelect: boolean) => void;
|
||||
}
|
||||
|
||||
export const useWallStore = create<WallState>((set, get) => ({
|
||||
@ -65,7 +67,6 @@ export const useWallStore = create<WallState>((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<WallState>((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<WallState>((set, get) => ({
|
||||
clearQueryWall: async () => {
|
||||
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