generated from template/vite-react-template
add wallnote
This commit is contained in:
parent
a91f80c1ba
commit
5ef42ee9de
10
README.md
10
README.md
@ -1 +1,9 @@
|
|||||||
# vite-react-template
|
# wallnote
|
||||||
|
|
||||||
|
ai快速开发,微应用。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
网页存储,灵感编辑。
|
BIN
docs/image.png
Normal file
BIN
docs/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 273 KiB |
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Wall Note</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "wallnote",
|
"name": "wallnote",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"user": "apps",
|
"user": "apps",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -12,7 +12,7 @@
|
|||||||
"deploy": "rsync -avz --delete dist/ light:~/apps/ai/dist",
|
"deploy": "rsync -avz --delete dist/ light:~/apps/ai/dist",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepub": "envision switchOrg apps",
|
"prepub": "envision switchOrg apps",
|
||||||
"pub": "envision deploy ./dist -k wallnote -v 0.0.2 -y y",
|
"pub": "envision deploy ./dist -k wallnote -v 0.0.3 -y y",
|
||||||
"ev": "npm run build && npm run deploy"
|
"ev": "npm run build && npm run deploy"
|
||||||
},
|
},
|
||||||
"stackblitz": {
|
"stackblitz": {
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"antd": "^5.24.1",
|
"antd": "^5.24.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -65,6 +65,9 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.11.13
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
|
github-markdown-css:
|
||||||
|
specifier: ^5.8.1
|
||||||
|
version: 5.8.1
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.11.1
|
specifier: ^11.11.1
|
||||||
version: 11.11.1
|
version: 11.11.1
|
||||||
@ -1547,6 +1550,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
github-markdown-css@5.8.1:
|
||||||
|
resolution: {integrity: sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -4011,6 +4018,8 @@ snapshots:
|
|||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
github-markdown-css@5.8.1: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
@ -5,14 +5,16 @@ import { ToastContainer } from 'react-toastify';
|
|||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { List } from './pages/wall/pages/List';
|
import { List } from './pages/wall/pages/List';
|
||||||
import { Auth } from './modules/layouts/Auth';
|
import { Auth } from './modules/layouts/Auth';
|
||||||
|
import { basename } from './modules/basename';
|
||||||
|
import 'github-markdown-css/github-markdown.css';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BrowserRouter basename='/apps/wallnote'>
|
<BrowserRouter basename={basename}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Auth auth={false} />}>
|
<Route element={<Auth auth={false} />}>
|
||||||
<Route path='/' element={<Flow checkLogin={false}/>} />
|
<Route index path='/' element={<Flow checkLogin={false} />} />
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<Auth auth={true} />}>
|
<Route element={<Auth auth={true} />}>
|
||||||
|
@ -33,3 +33,13 @@ body {
|
|||||||
border: unset;
|
border: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body,.tiptap {
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
list-style: unset;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
}
|
@ -5,19 +5,29 @@ export const message = {
|
|||||||
toast.error(message, options);
|
toast.error(message, options);
|
||||||
},
|
},
|
||||||
success: (message: string, options?: ToastOptions) => {
|
success: (message: string, options?: ToastOptions) => {
|
||||||
toast.success(message, {
|
return toast.success(message, {
|
||||||
position: 'top-left',
|
position: 'top-left',
|
||||||
autoClose: 1000,
|
autoClose: 1000,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
warning: (message: string, options?: ToastOptions) => {
|
warning: (message: string, options?: ToastOptions) => {
|
||||||
toast.warning(message, options);
|
return toast.warning(message, options);
|
||||||
},
|
},
|
||||||
info: (message: string, options?: ToastOptions) => {
|
info: (message: string, options?: ToastOptions) => {
|
||||||
toast.info(message, options);
|
return toast.info(message, options);
|
||||||
},
|
},
|
||||||
default: (message: string, options?: ToastOptions) => {
|
default: (message: string, options?: ToastOptions) => {
|
||||||
toast(message, options);
|
return toast(message, options);
|
||||||
|
},
|
||||||
|
loading: (message: string, options?: ToastOptions) => {
|
||||||
|
return toast(message, {
|
||||||
|
position: 'top-left',
|
||||||
|
autoClose: false,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close: (id: number | string) => {
|
||||||
|
toast.dismiss(id);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
81
src/pages/wall/hooks/listen-copy.ts
Normal file
81
src/pages/wall/hooks/listen-copy.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { message } from '@/modules/message';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
export const parseIfJson = (str: string) => {
|
||||||
|
try {
|
||||||
|
const js = JSON.parse(str);
|
||||||
|
// 判断js是否是正规的json对象, 初略判断
|
||||||
|
if (js && typeof js === 'object') {
|
||||||
|
return js;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const clipboardRead = async () => {
|
||||||
|
const read = await navigator.clipboard.read();
|
||||||
|
const [clipboardItem] = read;
|
||||||
|
if (!clipboardItem) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const types = clipboardItem.types;
|
||||||
|
const typesDataList: { type: string; data: string; blob?: any; base64?: string }[] = [];
|
||||||
|
for (let i = 0; i < types.length; i++) {
|
||||||
|
const type = types[i];
|
||||||
|
const data = await clipboardItem.getType(type);
|
||||||
|
switch (type) {
|
||||||
|
case 'text/plain':
|
||||||
|
const textPlain = await data.text();
|
||||||
|
// const jsonContent = parseIfJson(textPlain);
|
||||||
|
// if (jsonContent) {
|
||||||
|
// typesDataList.push({ type: 'text/json', data: jsonContent, blob: data });
|
||||||
|
// } else {
|
||||||
|
// typesDataList.push({ type: 'text/plain', data: textPlain, blob: data });
|
||||||
|
// }
|
||||||
|
typesDataList.push({ type: 'text/plain', data: textPlain, blob: data });
|
||||||
|
break;
|
||||||
|
case 'text/html':
|
||||||
|
const textHtml = await data.text();
|
||||||
|
typesDataList.push({ type: 'text/html', data: textHtml, blob: data });
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
const imagePng = await data.arrayBuffer();
|
||||||
|
const arrayBufferToBase64 = (buffer) => {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
};
|
||||||
|
const imagePngBase64 = arrayBufferToBase64(imagePng);
|
||||||
|
const imageData = `data:image/png;base64,${imagePngBase64}`;
|
||||||
|
const imageHtml = `<img style="width: 100%; height: 100%;" src="data:${type};base64,${imagePngBase64}" />`;
|
||||||
|
typesDataList.push({ type, data: imageHtml, blob: data, base64: imageData });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message.error('暂不支持该类型粘贴');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typesDataList;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 监听 wind: ctrl+v mac: command+v的粘贴事件
|
||||||
|
*/
|
||||||
|
export const useListenPaster = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = async (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
||||||
|
const r = await clipboardRead();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', listener);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
@ -30,6 +30,8 @@ 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 { ContextMenu } from './modules/ContextMenu';
|
||||||
type NodeData = {
|
type NodeData = {
|
||||||
id: string;
|
id: string;
|
||||||
position: XYPosition;
|
position: XYPosition;
|
||||||
@ -41,11 +43,17 @@ export function FlowContent() {
|
|||||||
const wallStore = useWallStore((state) => state);
|
const wallStore = useWallStore((state) => state);
|
||||||
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 _onNodesChange = useCallback((changes: NodeChange[]) => {
|
const _onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||||
const [change] = changes;
|
const [change] = changes;
|
||||||
|
|
||||||
|
if (change.type === 'remove') {
|
||||||
|
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);
|
// console.log('position changes', change);
|
||||||
getNewNodes();
|
getNewNodes(false);
|
||||||
}
|
}
|
||||||
onNodesChange(changes);
|
onNodesChange(changes);
|
||||||
}, []);
|
}, []);
|
||||||
@ -60,9 +68,10 @@ export function FlowContent() {
|
|||||||
wallStore.setOpen(true);
|
wallStore.setOpen(true);
|
||||||
wallStore.setSelectedNode(node);
|
wallStore.setSelectedNode(node);
|
||||||
};
|
};
|
||||||
const getNewNodes = () => {
|
const getNewNodes = (showMessage = true) => {
|
||||||
const nodes = reactFlowInstance.getNodes();
|
const nodes = reactFlowInstance.getNodes();
|
||||||
wallStore.saveNodes(nodes);
|
console.log('showMessage', showMessage);
|
||||||
|
wallStore.saveNodes(nodes, { showMessage: showMessage });
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mount) {
|
if (mount) {
|
||||||
@ -70,6 +79,7 @@ export function FlowContent() {
|
|||||||
}
|
}
|
||||||
}, [nodes, mount]);
|
}, [nodes, mount]);
|
||||||
useTabNode();
|
useTabNode();
|
||||||
|
useListenPaster();
|
||||||
// 添加新节点的函数
|
// 添加新节点的函数
|
||||||
const onPaneDoubleClick = (event) => {
|
const onPaneDoubleClick = (event) => {
|
||||||
// 计算节点位置
|
// 计算节点位置
|
||||||
@ -78,19 +88,18 @@ export function FlowContent() {
|
|||||||
const postion = reactFlowInstance.screenToFlowPosition({ x, y });
|
const postion = reactFlowInstance.screenToFlowPosition({ x, y });
|
||||||
const newNode = {
|
const newNode = {
|
||||||
id: randomId(), // 确保每个节点有唯一的ID
|
id: randomId(), // 确保每个节点有唯一的ID
|
||||||
type: 'wall', // 节点类型
|
type: 'wallnote', // 节点类型
|
||||||
position: postion, // 使用事件的客户端坐标
|
position: postion, // 使用事件的客户端坐标
|
||||||
data: { html: BlankNoteText },
|
data: { html: BlankNoteText },
|
||||||
};
|
};
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
const newNodes = nds.concat(newNode);
|
const newNodes = nds.concat(newNode);
|
||||||
getNewNodes();
|
|
||||||
return newNodes;
|
return newNodes;
|
||||||
});
|
});
|
||||||
message.success('添加节点成功');
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wallStore.setSelectedNode(newNode);
|
wallStore.setSelectedNode(newNode);
|
||||||
wallStore.setOpen(true);
|
wallStore.setOpen(true);
|
||||||
|
getNewNodes();
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
const hasFoucedNode = useMemo(() => {
|
const hasFoucedNode = useMemo(() => {
|
||||||
@ -99,6 +108,13 @@ export function FlowContent() {
|
|||||||
const { onCheckPanelDoubleClick } = useCheckDoubleClick({
|
const { onCheckPanelDoubleClick } = useCheckDoubleClick({
|
||||||
onPaneDoubleClick,
|
onPaneDoubleClick,
|
||||||
});
|
});
|
||||||
|
const handleContextMenu = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenu({ x: event.clientX, y: event.clientY });
|
||||||
|
};
|
||||||
|
const handleCloseContextMenu = () => {
|
||||||
|
setContextMenu(null);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@ -109,6 +125,7 @@ export function FlowContent() {
|
|||||||
onPaneClick={onCheckPanelDoubleClick}
|
onPaneClick={onCheckPanelDoubleClick}
|
||||||
zoomOnScroll={true}
|
zoomOnScroll={true}
|
||||||
preventScrolling={!hasFoucedNode}
|
preventScrolling={!hasFoucedNode}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
nodeTypes={CustomNodeType}>
|
nodeTypes={CustomNodeType}>
|
||||||
<Controls />
|
<Controls />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
@ -119,6 +136,7 @@ export function FlowContent() {
|
|||||||
<Panel>
|
<Panel>
|
||||||
<Drawer />
|
<Drawer />
|
||||||
<SaveModal />
|
<SaveModal />
|
||||||
|
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={showRef}
|
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={{
|
style={{
|
||||||
pointerEvents: selected ? 'auto' : 'none',
|
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) => {
|
const store = useStore((state) => {
|
||||||
return {
|
return {
|
||||||
updateWallRect: (id: string, rect: { width: number; height: number }) => {
|
updateWallRect: (id: string, rect: { width: number; height: number }) => {
|
||||||
@ -66,7 +69,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
state.setNodes(nodes);
|
state.setNodes(nodes);
|
||||||
wallStore.saveNodes(nodes);
|
save(nodes);
|
||||||
},
|
},
|
||||||
getNode: (id: string) => {
|
getNode: (id: string) => {
|
||||||
return state.nodes.find((node) => node.id === id);
|
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) => {
|
deleteNode: (id: string) => {
|
||||||
const nodes = state.nodes.filter((node) => node.id !== id);
|
const nodes = state.nodes.filter((node) => node.id !== id);
|
||||||
state.setNodes(nodes);
|
state.setNodes(nodes);
|
||||||
wallStore.saveNodes(nodes);
|
console.log('save', nodes, id);
|
||||||
|
save(nodes);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (selected) {
|
// if (selected) {
|
||||||
const handleDelete = (e: KeyboardEvent) => {
|
// const handleDelete = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Delete') {
|
// if (e.key === 'Delete') {
|
||||||
store.deleteNode(props.id);
|
// store.deleteNode(props.id);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
window.addEventListener('keydown', handleDelete);
|
// window.addEventListener('keydown', handleDelete);
|
||||||
return () => window.removeEventListener('keydown', handleDelete);
|
// return () => window.removeEventListener('keydown', handleDelete);
|
||||||
}
|
// }
|
||||||
}, [selected]);
|
// }, [selected]);
|
||||||
const width = data.width || 100;
|
const width = data.width || 100;
|
||||||
const height = data.height || 100;
|
const height = data.height || 100;
|
||||||
const style: React.CSSProperties = {};
|
const style: React.CSSProperties = {};
|
||||||
@ -96,7 +100,12 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
style.height = height;
|
style.height = height;
|
||||||
const showOpen = () => {
|
const showOpen = () => {
|
||||||
const node = store.getNode(props.id);
|
const node = store.getNode(props.id);
|
||||||
|
console.log('node eidt', node);
|
||||||
if (node) {
|
if (node) {
|
||||||
|
if (node.data?.noEdit) {
|
||||||
|
message.error('不支持编辑');
|
||||||
|
return;
|
||||||
|
}
|
||||||
wallStore.setOpen(true);
|
wallStore.setOpen(true);
|
||||||
wallStore.setSelectedNode(node);
|
wallStore.setSelectedNode(node);
|
||||||
} else {
|
} else {
|
||||||
@ -146,5 +155,5 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
};
|
};
|
||||||
export const WallNoteNode = memo(CustomNode);
|
export const WallNoteNode = memo(CustomNode);
|
||||||
export const CustomNodeType = {
|
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));
|
const newNodes = storeApi.getState().nodes.map((node) => (node.id === selectedNode.id ? selectedNode : node));
|
||||||
storeApi.setState({ nodes: newNodes });
|
storeApi.setState({ nodes: newNodes });
|
||||||
if (wallStore.id) {
|
if (wallStore.id) {
|
||||||
message.success('保存成功', {
|
message.success('保存到服务器成功', {
|
||||||
|
closeOnClick: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.success('保存到本地成功', {
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
wallStore.saveNodes(newNodes);
|
wallStore.saveNodes(newNodes, { showMessage: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let html = selectedNode?.data?.html || '';
|
let html = selectedNode?.data?.html || '';
|
||||||
|
@ -104,18 +104,21 @@ export const SaveModal = () => {
|
|||||||
description: values.description,
|
description: values.description,
|
||||||
summary: values.summary,
|
summary: values.summary,
|
||||||
tags: values.tags,
|
tags: values.tags,
|
||||||
markType: 'wall' as 'wall',
|
markType: 'wallnote' as 'wallnote',
|
||||||
data,
|
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) {
|
if (res.code === 200) {
|
||||||
setShowFormDialog(false);
|
setShowFormDialog(false);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
// 新创建
|
// 新创建
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
|
message.info('redirect to edit page');
|
||||||
wallStore.clear();
|
wallStore.clear();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(`/wall/${data.id}`);
|
navigate(`/edit/${data.id}`);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} 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 { useEffect, useState } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useWallStore } from '../../store/wall';
|
import { useWallStore } from '../../store/wall';
|
||||||
@ -26,6 +26,14 @@ export const ToolbarItem = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export type MenuItem = {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClick: () => any;
|
||||||
|
};
|
||||||
// 空白处点击,当不包函toolbar时候,关闭toolbar
|
// 空白处点击,当不包函toolbar时候,关闭toolbar
|
||||||
export const useBlankClick = () => {
|
export const useBlankClick = () => {
|
||||||
const { setToolbarOpen } = useWallStore(
|
const { setToolbarOpen } = useWallStore(
|
||||||
@ -61,14 +69,7 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
const store = useStore((state) => state);
|
const store = useStore((state) => state);
|
||||||
const hasLogin = !!userWallStore.user;
|
const hasLogin = !!userWallStore.user;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
type MenuItem = {
|
|
||||||
label: string;
|
|
||||||
key: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
onClick: () => any;
|
|
||||||
};
|
|
||||||
const menuList: MenuItem[] = [
|
const menuList: MenuItem[] = [
|
||||||
{
|
{
|
||||||
label: '导出',
|
label: '导出',
|
||||||
@ -97,7 +98,7 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = async (e) => {
|
||||||
const data = e.target?.result;
|
const data = e.target?.result;
|
||||||
const json = JSON.parse(data as string);
|
const json = JSON.parse(data as string);
|
||||||
const keys = ['id', 'type', 'position', 'data'];
|
const keys = ['id', 'type', 'position', 'data'];
|
||||||
@ -108,7 +109,10 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
});
|
});
|
||||||
const _nodes = [...nodes, ...newNodes];
|
const _nodes = [...nodes, ...newNodes];
|
||||||
store.setNodes(_nodes);
|
store.setNodes(_nodes);
|
||||||
wallStore.saveNodes(_nodes);
|
// window.location.reload();
|
||||||
|
wallStore.setNodes(_nodes);
|
||||||
|
await wallStore.saveNodes(_nodes);
|
||||||
|
message.success('导入成功');
|
||||||
} else {
|
} else {
|
||||||
message.error('文件格式错误');
|
message.error('文件格式错误');
|
||||||
}
|
}
|
||||||
@ -139,7 +143,16 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
if (hasLogin) {
|
||||||
|
menuList.unshift({
|
||||||
|
label: '我的笔记',
|
||||||
|
key: 'myWall',
|
||||||
|
icon: <BrickWall />,
|
||||||
|
onClick: () => {
|
||||||
|
navigate('/list');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!hasLogin) {
|
if (!hasLogin) {
|
||||||
menuList.push({
|
menuList.push({
|
||||||
label: '登录',
|
label: '登录',
|
||||||
@ -218,6 +231,7 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
menuList.push({
|
menuList.push({
|
||||||
label: '退出 ',
|
label: '退出 ',
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
|
@ -22,7 +22,11 @@ export const List = () => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className='p-4 bg-white w-full h-full flex flex-col'>
|
<div className='p-4 bg-white w-full h-full flex flex-col'>
|
||||||
<div className='flex justify-between h-10 items-center'>
|
<div
|
||||||
|
className='flex justify-between h-10 items-center'
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/');
|
||||||
|
}}>
|
||||||
<div className='text-2xl font-bold'>Wall Note</div>
|
<div className='text-2xl font-bold'>Wall Note</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col flex-grow overflow-hidden'>
|
<div className='flex flex-col flex-grow overflow-hidden'>
|
||||||
@ -33,7 +37,7 @@ export const List = () => {
|
|||||||
key={wall.id}
|
key={wall.id}
|
||||||
className='p-4 border border-gray-200 w-80 rounded-md'
|
className='p-4 border border-gray-200 w-80 rounded-md'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/wall/${wall.id}`);
|
navigate(`/edit/${wall.id}`);
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<div>{wall.title}</div>
|
<div>{wall.title}</div>
|
||||||
|
@ -58,7 +58,7 @@ export const useUserWallStore = create<UserWallStore>((set, get) => ({
|
|||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'list',
|
key: 'list',
|
||||||
markType: 'wall',
|
markType: 'wallnote',
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ interface WallState {
|
|||||||
// 只做传递
|
// 只做传递
|
||||||
nodes: NodeData[];
|
nodes: NodeData[];
|
||||||
setNodes: (nodes: NodeData[]) => void;
|
setNodes: (nodes: NodeData[]) => void;
|
||||||
saveNodes: (nodes: NodeData[]) => Promise<void>;
|
saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
selectedNode: NodeData | null;
|
selectedNode: NodeData | null;
|
||||||
@ -67,10 +67,13 @@ export const useWallStore = create<WallState>((set, get) => ({
|
|||||||
setNodes: (nodes) => {
|
setNodes: (nodes) => {
|
||||||
set({ nodes });
|
set({ nodes });
|
||||||
},
|
},
|
||||||
saveNodes: async (nodes: NodeData[]) => {
|
saveNodes: async (nodes: NodeData[], opts) => {
|
||||||
|
console.log('nodes', nodes, opts, opts?.showMessage ?? true);
|
||||||
if (!get().id) {
|
if (!get().id) {
|
||||||
const covertData = getNodeData(nodes);
|
const covertData = getNodeData(nodes);
|
||||||
setWallData({ nodes: covertData });
|
setWallData({ nodes: covertData });
|
||||||
|
const showMessage = opts?.showMessage ?? true;
|
||||||
|
showMessage && message.success('保存到本地');
|
||||||
} else {
|
} else {
|
||||||
const { id } = get();
|
const { id } = get();
|
||||||
const userWallStore = useUserWallStore.getState();
|
const userWallStore = useUserWallStore.getState();
|
||||||
|
31
src/pages/wall/utils/get-image-rect.ts
Normal file
31
src/pages/wall/utils/get-image-rect.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const getImageWidthHeightByBase64 = async (
|
||||||
|
b64str: any,
|
||||||
|
): Promise<{
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 创建 Canvas 对象
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
// 创建 Image 对象
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const width = img.width;
|
||||||
|
const height = img.height;
|
||||||
|
console.log(`宽度: ${width}, 高度: ${height}`);
|
||||||
|
resolve({ width, height });
|
||||||
|
canvas.remove();
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
console.error('无法加载图片');
|
||||||
|
reject(new Error('无法加载图片'));
|
||||||
|
canvas.remove();
|
||||||
|
};
|
||||||
|
img.src = b64str;
|
||||||
|
});
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user