generated from template/vite-react-template
	add wallnote
This commit is contained in:
		
							
								
								
									
										170
									
								
								src/pages/wall/modules/ContextMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/pages/wall/modules/ContextMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { ToolbarItem, MenuItem } from './toolbar/Toolbar';
 | 
			
		||||
import { ClipboardPaste } from 'lucide-react';
 | 
			
		||||
import { clipboardRead } from '../hooks/listen-copy';
 | 
			
		||||
import { useReactFlow, useStore } from '@xyflow/react';
 | 
			
		||||
import { randomId } from '../utils/random';
 | 
			
		||||
import { message } from '@/modules/message';
 | 
			
		||||
import { useWallStore } from '../store/wall';
 | 
			
		||||
import { useShallow } from 'zustand/react/shallow';
 | 
			
		||||
import { min, max } from 'lodash-es';
 | 
			
		||||
import { getImageWidthHeightByBase64 } from '../utils/get-image-rect';
 | 
			
		||||
interface ContextMenuProps {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
class HasTypeCheck {
 | 
			
		||||
  constructor(list: any[]) {
 | 
			
		||||
    this.list = list;
 | 
			
		||||
  }
 | 
			
		||||
  list: { type?: string; data: any }[];
 | 
			
		||||
  hasType = (type = 'type/html') => {
 | 
			
		||||
    return this.list.some((item) => item.type === type);
 | 
			
		||||
  };
 | 
			
		||||
  getType = (type = 'type/html') => {
 | 
			
		||||
    return this.list.find((item) => item.type === type);
 | 
			
		||||
  };
 | 
			
		||||
  getText = () => {
 | 
			
		||||
    const hasHtml = this.hasType('text/html');
 | 
			
		||||
    if (hasHtml) {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 200,
 | 
			
		||||
        data: this.getType('text/html')?.data || '',
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    const hasText = this.hasType('text/plain');
 | 
			
		||||
    if (hasText) {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 200,
 | 
			
		||||
        data: this.getType('text/plain')?.data || '',
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      code: 404,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  getJson() {
 | 
			
		||||
    const hasJson = this.hasType('text/json');
 | 
			
		||||
    if (hasJson) {
 | 
			
		||||
      const data = this.getType('text/json')?.data || '';
 | 
			
		||||
      return {
 | 
			
		||||
        code: 200,
 | 
			
		||||
        data: data,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      code: 404,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => {
 | 
			
		||||
  const reactFlowInstance = useReactFlow();
 | 
			
		||||
  const store = useStore((state) => state);
 | 
			
		||||
  const wallStore = useWallStore(
 | 
			
		||||
    useShallow((state) => {
 | 
			
		||||
      return {
 | 
			
		||||
        setNodes: state.setNodes,
 | 
			
		||||
        saveNodes: state.saveNodes,
 | 
			
		||||
      };
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  // const
 | 
			
		||||
  const menuList: MenuItem[] = [
 | 
			
		||||
    {
 | 
			
		||||
      label: '粘贴',
 | 
			
		||||
      icon: <ClipboardPaste />,
 | 
			
		||||
      key: 'paste',
 | 
			
		||||
      onClick: async () => {
 | 
			
		||||
        const readList = await clipboardRead();
 | 
			
		||||
        const check = new HasTypeCheck(readList);
 | 
			
		||||
        if (readList.length <= 0) {
 | 
			
		||||
          message.error('粘贴为空');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        let content: string = '';
 | 
			
		||||
        let hasContent = false;
 | 
			
		||||
        const text = check.getText();
 | 
			
		||||
        let width = 100;
 | 
			
		||||
        let height = 100;
 | 
			
		||||
        if (text.code === 200) {
 | 
			
		||||
          content = text.data;
 | 
			
		||||
          hasContent = true;
 | 
			
		||||
          width = min([content.length * 16, 600])!;
 | 
			
		||||
          height = max([200, (content.length * 16) / 400])!;
 | 
			
		||||
        }
 | 
			
		||||
        console.log('result', readList);
 | 
			
		||||
        if (!hasContent) {
 | 
			
		||||
          const json = check.getJson();
 | 
			
		||||
          if (json.code === 200) {
 | 
			
		||||
            content = JSON.stringify(json.data, null, 2);
 | 
			
		||||
            hasContent = true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        let noEdit = false;
 | 
			
		||||
        if (!hasContent) {
 | 
			
		||||
          content = readList[0].data || '';
 | 
			
		||||
          const base64 = readList[0].base64;
 | 
			
		||||
          const rect = await getImageWidthHeightByBase64(base64);
 | 
			
		||||
          width = rect.width;
 | 
			
		||||
          height = rect.height;
 | 
			
		||||
          noEdit = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const flowPosition = reactFlowInstance.screenToFlowPosition({ x, y });
 | 
			
		||||
        const nodes = store.nodes;
 | 
			
		||||
        const newNodeData: any = {
 | 
			
		||||
          id: randomId(),
 | 
			
		||||
          type: 'wallnote',
 | 
			
		||||
          position: flowPosition,
 | 
			
		||||
          data: {
 | 
			
		||||
            width,
 | 
			
		||||
            height,
 | 
			
		||||
            html: content,
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
        if (noEdit) {
 | 
			
		||||
          newNodeData.data.noEdit = true;
 | 
			
		||||
        }
 | 
			
		||||
        const newNodes = [newNodeData];
 | 
			
		||||
        const _nodes = [...nodes, ...newNodes];
 | 
			
		||||
        wallStore.setNodes(_nodes);
 | 
			
		||||
        wallStore.saveNodes(_nodes);
 | 
			
		||||
        // reactFlowInstance.setNodes(_nodes);
 | 
			
		||||
      },
 | 
			
		||||
    }, //
 | 
			
		||||
  ];
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        top: y - 20,
 | 
			
		||||
        left: x - 20,
 | 
			
		||||
        backgroundColor: 'white',
 | 
			
		||||
        border: '1px solid #ccc',
 | 
			
		||||
        width: 200,
 | 
			
		||||
        zIndex: 1000,
 | 
			
		||||
      }}
 | 
			
		||||
      onMouseLeave={onClose}>
 | 
			
		||||
      {menuList.map((item) => (
 | 
			
		||||
        <ToolbarItem
 | 
			
		||||
          key={item.key}
 | 
			
		||||
          className={item.className}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            item.onClick?.();
 | 
			
		||||
          }}>
 | 
			
		||||
          {item.children ? (
 | 
			
		||||
            <>{item.children}</>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              <div>{item.icon}</div>
 | 
			
		||||
              <div>{item.label}</div>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </ToolbarItem>
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ContextMenu;
 | 
			
		||||
@@ -34,7 +34,7 @@ const ShowContent = (props: { data: WallData; selected: boolean }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      ref={showRef}
 | 
			
		||||
      className='p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white'
 | 
			
		||||
      className='p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white markdown-body'
 | 
			
		||||
      style={{
 | 
			
		||||
        pointerEvents: selected ? 'auto' : 'none',
 | 
			
		||||
      }}
 | 
			
		||||
@@ -55,6 +55,9 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
 | 
			
		||||
      };
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
  const save = (nodes: any[]) => {
 | 
			
		||||
    wallStore.saveNodes(nodes);
 | 
			
		||||
  };
 | 
			
		||||
  const store = useStore((state) => {
 | 
			
		||||
    return {
 | 
			
		||||
      updateWallRect: (id: string, rect: { width: number; height: number }) => {
 | 
			
		||||
@@ -66,7 +69,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
 | 
			
		||||
          return node;
 | 
			
		||||
        });
 | 
			
		||||
        state.setNodes(nodes);
 | 
			
		||||
        wallStore.saveNodes(nodes);
 | 
			
		||||
        save(nodes);
 | 
			
		||||
      },
 | 
			
		||||
      getNode: (id: string) => {
 | 
			
		||||
        return state.nodes.find((node) => node.id === id);
 | 
			
		||||
@@ -74,21 +77,22 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
 | 
			
		||||
      deleteNode: (id: string) => {
 | 
			
		||||
        const nodes = state.nodes.filter((node) => node.id !== id);
 | 
			
		||||
        state.setNodes(nodes);
 | 
			
		||||
        wallStore.saveNodes(nodes);
 | 
			
		||||
        console.log('save', nodes, id);
 | 
			
		||||
        save(nodes);
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selected) {
 | 
			
		||||
      const handleDelete = (e: KeyboardEvent) => {
 | 
			
		||||
        if (e.key === 'Delete') {
 | 
			
		||||
          store.deleteNode(props.id);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      window.addEventListener('keydown', handleDelete);
 | 
			
		||||
      return () => window.removeEventListener('keydown', handleDelete);
 | 
			
		||||
    }
 | 
			
		||||
  }, [selected]);
 | 
			
		||||
  // useEffect(() => {
 | 
			
		||||
  //   if (selected) {
 | 
			
		||||
  //     const handleDelete = (e: KeyboardEvent) => {
 | 
			
		||||
  //       if (e.key === 'Delete') {
 | 
			
		||||
  //         store.deleteNode(props.id);
 | 
			
		||||
  //       }
 | 
			
		||||
  //     };
 | 
			
		||||
  //     window.addEventListener('keydown', handleDelete);
 | 
			
		||||
  //     return () => window.removeEventListener('keydown', handleDelete);
 | 
			
		||||
  //   }
 | 
			
		||||
  // }, [selected]);
 | 
			
		||||
  const width = data.width || 100;
 | 
			
		||||
  const height = data.height || 100;
 | 
			
		||||
  const style: React.CSSProperties = {};
 | 
			
		||||
@@ -96,7 +100,12 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
 | 
			
		||||
  style.height = height;
 | 
			
		||||
  const showOpen = () => {
 | 
			
		||||
    const node = store.getNode(props.id);
 | 
			
		||||
    console.log('node eidt', node);
 | 
			
		||||
    if (node) {
 | 
			
		||||
      if (node.data?.noEdit) {
 | 
			
		||||
        message.error('不支持编辑');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      wallStore.setOpen(true);
 | 
			
		||||
      wallStore.setSelectedNode(node);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -146,5 +155,5 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
 | 
			
		||||
};
 | 
			
		||||
export const WallNoteNode = memo(CustomNode);
 | 
			
		||||
export const CustomNodeType = {
 | 
			
		||||
  wall: WallNoteNode,
 | 
			
		||||
  wallnote: WallNoteNode,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -61,11 +61,15 @@ const Drawer = () => {
 | 
			
		||||
      const newNodes = storeApi.getState().nodes.map((node) => (node.id === selectedNode.id ? selectedNode : node));
 | 
			
		||||
      storeApi.setState({ nodes: newNodes });
 | 
			
		||||
      if (wallStore.id) {
 | 
			
		||||
        message.success('保存成功', {
 | 
			
		||||
        message.success('保存到服务器成功', {
 | 
			
		||||
          closeOnClick: true,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        message.success('保存到本地成功', {
 | 
			
		||||
          closeOnClick: true,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      wallStore.saveNodes(newNodes);
 | 
			
		||||
      wallStore.saveNodes(newNodes, { showMessage: false });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  let html = selectedNode?.data?.html || '';
 | 
			
		||||
 
 | 
			
		||||
@@ -104,18 +104,21 @@ export const SaveModal = () => {
 | 
			
		||||
      description: values.description,
 | 
			
		||||
      summary: values.summary,
 | 
			
		||||
      tags: values.tags,
 | 
			
		||||
      markType: 'wall' as 'wall',
 | 
			
		||||
      markType: 'wallnote' as 'wallnote',
 | 
			
		||||
      data,
 | 
			
		||||
    };
 | 
			
		||||
    const res = await userWallStore.saveWall(fromData, { refresh: true });
 | 
			
		||||
    const loading = message.loading('保存中...');
 | 
			
		||||
    const res = await userWallStore.saveWall(fromData, { refresh: false });
 | 
			
		||||
    message.close(loading);
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      setShowFormDialog(false);
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        // 新创建
 | 
			
		||||
        const data = res.data;
 | 
			
		||||
        message.info('redirect to edit page');
 | 
			
		||||
        wallStore.clear();
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          navigate(`/wall/${data.id}`);
 | 
			
		||||
          navigate(`/edit/${data.id}`);
 | 
			
		||||
        }, 2000);
 | 
			
		||||
      } else {
 | 
			
		||||
        // 编辑
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { PanelTopOpen, PanelTopClose, Save, Download, Upload, User, Trash, Plus } from 'lucide-react';
 | 
			
		||||
import { PanelTopOpen, PanelTopClose, Save, Download, Upload, User, Trash, Plus, BrickWall } from 'lucide-react';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useShallow } from 'zustand/react/shallow';
 | 
			
		||||
import { useWallStore } from '../../store/wall';
 | 
			
		||||
@@ -26,6 +26,14 @@ export const ToolbarItem = ({
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
export type MenuItem = {
 | 
			
		||||
  label: string;
 | 
			
		||||
  key: string;
 | 
			
		||||
  icon?: React.ReactNode;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
  className?: string;
 | 
			
		||||
  onClick: () => any;
 | 
			
		||||
};
 | 
			
		||||
// 空白处点击,当不包函toolbar时候,关闭toolbar
 | 
			
		||||
export const useBlankClick = () => {
 | 
			
		||||
  const { setToolbarOpen } = useWallStore(
 | 
			
		||||
@@ -61,14 +69,7 @@ export const ToolbarContent = ({ open }) => {
 | 
			
		||||
  const store = useStore((state) => state);
 | 
			
		||||
  const hasLogin = !!userWallStore.user;
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  type MenuItem = {
 | 
			
		||||
    label: string;
 | 
			
		||||
    key: string;
 | 
			
		||||
    icon?: React.ReactNode;
 | 
			
		||||
    children?: React.ReactNode;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    onClick: () => any;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const menuList: MenuItem[] = [
 | 
			
		||||
    {
 | 
			
		||||
      label: '导出',
 | 
			
		||||
@@ -97,7 +98,7 @@ export const ToolbarContent = ({ open }) => {
 | 
			
		||||
              const file = e.target.files?.[0];
 | 
			
		||||
              if (file) {
 | 
			
		||||
                const reader = new FileReader();
 | 
			
		||||
                reader.onload = (e) => {
 | 
			
		||||
                reader.onload = async (e) => {
 | 
			
		||||
                  const data = e.target?.result;
 | 
			
		||||
                  const json = JSON.parse(data as string);
 | 
			
		||||
                  const keys = ['id', 'type', 'position', 'data'];
 | 
			
		||||
@@ -108,7 +109,10 @@ export const ToolbarContent = ({ open }) => {
 | 
			
		||||
                    });
 | 
			
		||||
                    const _nodes = [...nodes, ...newNodes];
 | 
			
		||||
                    store.setNodes(_nodes);
 | 
			
		||||
                    wallStore.saveNodes(_nodes);
 | 
			
		||||
                    // window.location.reload();
 | 
			
		||||
                    wallStore.setNodes(_nodes);
 | 
			
		||||
                    await wallStore.saveNodes(_nodes);
 | 
			
		||||
                    message.success('导入成功');
 | 
			
		||||
                  } else {
 | 
			
		||||
                    message.error('文件格式错误');
 | 
			
		||||
                  }
 | 
			
		||||
@@ -139,7 +143,16 @@ export const ToolbarContent = ({ open }) => {
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  if (hasLogin) {
 | 
			
		||||
    menuList.unshift({
 | 
			
		||||
      label: '我的笔记',
 | 
			
		||||
      key: 'myWall',
 | 
			
		||||
      icon: <BrickWall />,
 | 
			
		||||
      onClick: () => {
 | 
			
		||||
        navigate('/list');
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (!hasLogin) {
 | 
			
		||||
    menuList.push({
 | 
			
		||||
      label: '登录',
 | 
			
		||||
@@ -218,6 +231,7 @@ export const ToolbarContent = ({ open }) => {
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    menuList.push({
 | 
			
		||||
      label: '退出  ',
 | 
			
		||||
      key: 'logout',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user