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