temp: fix bugs

This commit is contained in:
熊潇 2025-02-25 13:19:38 +08:00
parent 5ef42ee9de
commit a92e377d9f
15 changed files with 366 additions and 126 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "wallnote", "name": "wallnote",
"private": true, "private": true,
"version": "0.0.3", "version": "0.0.6",
"type": "module", "type": "module",
"user": "apps", "user": "apps",
"scripts": { "scripts": {
@ -11,8 +11,8 @@
"lint": "eslint .", "lint": "eslint .",
"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": "pnpm build && envision switch apps",
"pub": "envision deploy ./dist -k wallnote -v 0.0.3 -y y", "pub": "envision deploy ./dist -k wallnote -v 0.0.6 -y y",
"ev": "npm run build && npm run deploy" "ev": "npm run build && npm run deploy"
}, },
"stackblitz": { "stackblitz": {

View File

@ -1,5 +1,5 @@
import { Flow } from './pages/wall'; import { Flow } from './pages/wall';
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Editor } from './pages/editor'; import { Editor } from './pages/editor';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
@ -7,7 +7,7 @@ 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 { basename } from './modules/basename';
import 'github-markdown-css/github-markdown.css'; import 'github-markdown-css/github-markdown.css';
import { App as WallShareApp } from './pages/wall-share';
export const App = () => { export const App = () => {
return ( return (
<> <>
@ -21,6 +21,16 @@ export const App = () => {
<Route path='/edit/:id' element={<Flow checkLogin={true} />} /> <Route path='/edit/:id' element={<Flow checkLogin={true} />} />
<Route path='/list' element={<List />} /> <Route path='/list' element={<List />} />
</Route> </Route>
<Route
path='/share/*'
element={
<Auth auth={false}>
<WallShareApp />
</Auth>
}
/>
<Route path='*' element={<Navigate to='/' />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
<ToastContainer /> <ToastContainer />

View File

@ -3,7 +3,13 @@ import { useUserWallStore } from '../../pages/wall/store/user-wall';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
export const Auth = ({ children, auth = true }: { children?: React.ReactNode; auth?: boolean }) => { /**
*
* @param children
* @param auth
* @returns
*/
export const Auth = ({ children, auth = true }: { children?: React.ReactNode; auth?: boolean; canNoAuth?: boolean; isOutlet?: boolean }) => {
const userStore = useUserWallStore( const userStore = useUserWallStore(
useShallow((state) => { useShallow((state) => {
return { user: state.user, queryMe: state.queryMe }; return { user: state.user, queryMe: state.queryMe };
@ -20,5 +26,8 @@ export const Auth = ({ children, auth = true }: { children?: React.ReactNode; au
} }
return <>{children}</>; return <>{children}</>;
} }
if (auth) {
return <>{userStore.user && <Outlet />}</>;
}
return <>{<Outlet />}</>; return <>{<Outlet />}</>;
}; };

View File

@ -0,0 +1,13 @@
import { Routes, Route } from 'react-router-dom';
const WallShare = () => {
return <div>WallShare</div>;
};
export const App = () => {
return (
<Routes>
<Route path='/:id' element={<WallShare />} />
</Routes>
);
};

View File

@ -0,0 +1,24 @@
import { ToastContentProps } from 'react-toastify';
export function SplitButtons({ closeToast }: ToastContentProps) {
return (
// using a grid with 3 columns
<div className='grid grid-cols-[1fr_1px_80px] w-full'>
<div className='flex flex-col p-4'>
<h3 className='text-zinc-800 text-sm font-semibold'></h3>
<p className='text-sm'></p>
</div>
{/* that's the vertical line which separate the text and the buttons*/}
<div className='bg-zinc-900/20 h-full' />
<div className='grid grid-rows-[1fr_1px_1fr] h-full'>
{/*specifying a custom closure reason that can be used with the onClose callback*/}
<button onClick={() => closeToast('cancle')} className='text-purple-600'>
</button>
<div className='bg-zinc-900/20 w-full' />
{/*specifying a custom closure reason that can be used with the onClose callback*/}
<button onClick={() => closeToast('success')}></button>
</div>
</div>
);
}

12
src/pages/wall/docs.ts Normal file
View File

@ -0,0 +1,12 @@
export const DOCS_NODE = {
id: 'e15owpuh9cv3fgwx5zymtc',
data: {
html: '<h1>Wallnote 基本使用介绍 v0.0.6</h1><p></p><p>可拖拽的随笔记功能。</p><ul class="tight" data-tight="true"><li><p>纯网页界面,数据存储在浏览器(不登陆情况下,只有单个页面)</p></li><li><p>这个墙随便拖动</p></li><li><p>双击空格添加一条记录并打开编辑esc关闭</p></li><li><p>富文本编辑器md语法</p></li><li><p>点击节点聚焦后delete删除</p></li><li><p>右键空白处粘贴</p><ul class="tight" data-tight="true"><li><p>html的内容编辑会丢失样式</p></li><li><p>图片的内容(粘贴后不能编辑)</p></li><li><p>文本内容</p></li><li><p>复制的节点信息</p></li></ul></li><li><p>边框可拖动大小</p></li></ul><h3>注意</h3><ul class="tight" data-tight="true"><li><p>点击节点聚焦后,如果有滚动条,节点内容才能滚动</p></li><li><p>图片复制只能是二进制文件夹的图片复制后无效。比如snipaste 贴图复制Can To Do。</p></li></ul><p></p><h2>登录后功能</h2><ul class="tight" data-tight="true"><li><p>保存而不是临时编辑</p></li></ul><p></p><h2>TODO</h2><ul class="tight" data-tight="true"><li><p>do do do</p></li><li><p>ai ++++</p></li></ul>',
width: 583,
height: 448,
},
type: 'wallnote',
position: { x: -901.1464949275596, y: -672.8095405534519 },
measured: { width: 583, height: 448 },
selected: true,
};

View File

@ -26,13 +26,12 @@ export const clipboardRead = async () => {
switch (type) { switch (type) {
case 'text/plain': case 'text/plain':
const textPlain = await data.text(); const textPlain = await data.text();
// const jsonContent = parseIfJson(textPlain); const jsonContent = parseIfJson(textPlain);
// if (jsonContent) { if (jsonContent && jsonContent.type === 'wallnote') {
// typesDataList.push({ type: 'text/json', data: jsonContent, blob: data }); typesDataList.push({ type: 'text/json', data: jsonContent, blob: data });
// } else { } else {
// typesDataList.push({ type: 'text/plain', data: textPlain, blob: data }); typesDataList.push({ type: 'text/plain', data: textPlain, blob: data });
// } }
typesDataList.push({ type: 'text/plain', data: textPlain, blob: data });
break; break;
case 'text/html': case 'text/html':
const textHtml = await data.text(); const textHtml = await data.text();

View File

@ -40,7 +40,15 @@ 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((state) => state); const wallStore = useWallStore(
useShallow((state) => {
return {
nodes: state.nodes,
saveNodes: state.saveNodes,
checkAndOpen: state.checkAndOpen,
};
}),
);
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);
@ -65,8 +73,7 @@ export function FlowContent() {
}; };
}, [wallStore.nodes]); }, [wallStore.nodes]);
const onNodeDoubleClick = (event, node) => { const onNodeDoubleClick = (event, node) => {
wallStore.setOpen(true); wallStore.checkAndOpen(true, node);
wallStore.setSelectedNode(node);
}; };
const getNewNodes = (showMessage = true) => { const getNewNodes = (showMessage = true) => {
const nodes = reactFlowInstance.getNodes(); const nodes = reactFlowInstance.getNodes();
@ -79,7 +86,7 @@ export function FlowContent() {
} }
}, [nodes, mount]); }, [nodes, mount]);
useTabNode(); useTabNode();
useListenPaster(); // useListenPaster();
// 添加新节点的函数 // 添加新节点的函数
const onPaneDoubleClick = (event) => { const onPaneDoubleClick = (event) => {
// 计算节点位置 // 计算节点位置
@ -97,8 +104,7 @@ export function FlowContent() {
return newNodes; return newNodes;
}); });
setTimeout(() => { setTimeout(() => {
wallStore.setSelectedNode(newNode); wallStore.checkAndOpen(true, newNode);
wallStore.setOpen(true);
getNewNodes(); getNewNodes();
}, 200); }, 200);
}; };
@ -126,6 +132,8 @@ export function FlowContent() {
zoomOnScroll={true} zoomOnScroll={true}
preventScrolling={!hasFoucedNode} preventScrolling={!hasFoucedNode}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
minZoom={0.05}
maxZoom={20}
nodeTypes={CustomNodeType}> nodeTypes={CustomNodeType}>
<Controls /> <Controls />
<MiniMap /> <MiniMap />
@ -149,6 +157,7 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
return { return {
loaded: state.loaded, loaded: state.loaded,
init: state.init, init: state.init,
clearId: state.clearId,
}; };
}), }),
); );
@ -168,6 +177,7 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
variant='contained' variant='contained'
onClick={() => { onClick={() => {
navigate('/'); navigate('/');
wallStore.clearId();
}}> }}>
</Button> </Button>

View File

@ -1,24 +1,41 @@
import React from 'react'; import React, { useMemo } from 'react';
import { ToolbarItem, MenuItem } from './toolbar/Toolbar'; import { ToolbarItem, MenuItem } from './toolbar/Toolbar';
import { ClipboardPaste } from 'lucide-react'; import { ClipboardPaste, Copy } from 'lucide-react';
import { clipboardRead } from '../hooks/listen-copy'; import { clipboardRead } from '../hooks/listen-copy';
import { useReactFlow, useStore } from '@xyflow/react'; import { useReactFlow, useStore } from '@xyflow/react';
import { randomId } from '../utils/random'; import { randomId } from '../utils/random';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { useWallStore } from '../store/wall'; import { useWallStore } from '../store/wall';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { min, max } from 'lodash-es'; import { getImageWidthHeightByBase64, getTextWidthHeight } from '../utils/get-image-rect';
import { getImageWidthHeightByBase64 } from '../utils/get-image-rect';
interface ContextMenuProps { interface ContextMenuProps {
x: number; x: number;
y: number; y: number;
onClose: () => void; onClose: () => void;
} }
type NewNodeData = {
id: string;
type: 'wallnote';
position: { x: number; y: number };
data: {
width: number;
height: number;
html: string;
dataType?: string;
};
};
class HasTypeCheck { class HasTypeCheck {
constructor(list: any[]) { newNodeData: NewNodeData;
constructor(list: any[], position: { x: number; y: number }) {
this.list = list; this.list = list;
this.newNodeData = {
id: randomId(),
type: 'wallnote',
position,
data: { width: 0, height: 0, html: '' },
};
} }
list: { type?: string; data: any }[]; list: { type?: string; data: any; base64?: string }[];
hasType = (type = 'type/html') => { hasType = (type = 'type/html') => {
return this.list.some((item) => item.type === type); return this.list.some((item) => item.type === type);
}; };
@ -30,14 +47,20 @@ class HasTypeCheck {
if (hasHtml) { if (hasHtml) {
return { return {
code: 200, code: 200,
data: this.getType('text/html')?.data || '', data: {
html: this.getType('text/html')?.data || '',
dataType: 'text/html',
},
}; };
} }
const hasText = this.hasType('text/plain'); const hasText = this.hasType('text/plain');
if (hasText) { if (hasText) {
return { return {
code: 200, code: 200,
data: this.getType('text/plain')?.data || '', data: {
html: this.getType('text/plain')?.data || '',
dataType: 'text/plain',
},
}; };
} }
return { return {
@ -57,6 +80,53 @@ class HasTypeCheck {
code: 404, code: 404,
}; };
} }
async getData() {
const json = this.getJson();
if (json.code === 200) {
if (json.data.type === 'wallnote') {
const { selected, ...rest } = json.data;
const newNodeData = {
...this.newNodeData,
...rest,
id: this.newNodeData.id,
position: this.newNodeData.position,
};
this.newNodeData = newNodeData;
return this.newNodeData;
} else {
this.newNodeData.data.html = JSON.stringify(json.data, null, 2);
return this.newNodeData;
}
}
const text = this.getText();
if (text.code === 200) {
const { html, dataType } = text.data || { html: '', dataType: 'text/html' };
this.newNodeData.data.html = html;
let maxWidth = 600;
let fontSize = 16;
let maxHeight = 400;
let minHeight = 100;
if (dataType === 'text/html') {
maxWidth = 400;
fontSize = 10;
maxHeight = 200;
minHeight = 50;
}
const wh = await getTextWidthHeight({ str: html, width: 400, maxHeight, minHeight, fontSize });
this.newNodeData.data.width = wh.width;
this.newNodeData.data.height = wh.height;
return this.newNodeData;
}
// 图片
const { base64, type } = this.list[0];
const rect = await getImageWidthHeightByBase64(base64);
this.newNodeData.data.width = rect.width;
this.newNodeData.data.height = rect.height;
this.newNodeData.data.dataType = type;
this.newNodeData.data.html = `<img src="${base64}" alt="图片" />`;
return this.newNodeData;
}
} }
export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => { export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => {
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
@ -69,71 +139,53 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => {
}; };
}), }),
); );
// const const copyMenu = {
const menuList: MenuItem[] = [ label: '复制',
{ icon: <Copy />,
label: '粘贴', key: 'copy',
icon: <ClipboardPaste />, onClick: async () => {
key: 'paste', const nodes = reactFlowInstance.getNodes();
onClick: async () => { const selectedNode = nodes.find((node) => node.selected);
const readList = await clipboardRead(); if (!selectedNode) {
const check = new HasTypeCheck(readList); message.error('没有选中节点');
if (readList.length <= 0) { return;
message.error('粘贴为空'); }
return; const copyData = JSON.stringify(selectedNode);
} navigator.clipboard.writeText(copyData);
let content: string = ''; message.success('复制成功');
let hasContent = false; setTimeout(() => {
const text = check.getText(); onClose();
let width = 100; }, 1000);
let height = 100; },
if (text.code === 200) { };
content = text.data; const pasteMenu = {
hasContent = true; label: '粘贴',
width = min([content.length * 16, 600])!; icon: <ClipboardPaste />,
height = max([200, (content.length * 16) / 400])!; key: 'paste',
} onClick: async () => {
console.log('result', readList); const readList = await clipboardRead();
if (!hasContent) { const flowPosition = reactFlowInstance.screenToFlowPosition({ x, y });
const json = check.getJson(); const check = new HasTypeCheck(readList, flowPosition);
if (json.code === 200) { if (readList.length <= 0) {
content = JSON.stringify(json.data, null, 2); message.error('粘贴为空');
hasContent = true; return;
} }
} const newNodeData = await check.getData();
let noEdit = false; const nodes = store.nodes;
if (!hasContent) { const _nodes = [...nodes, newNodeData];
content = readList[0].data || ''; wallStore.setNodes(_nodes);
const base64 = readList[0].base64; wallStore.saveNodes(_nodes);
const rect = await getImageWidthHeightByBase64(base64); // reactFlowInstance.setNodes(_nodes);
width = rect.width; },
height = rect.height; };
noEdit = true; const menuList = useMemo(() => {
} const selected = store.nodes.find((node) => node.selected);
if (selected) {
return [copyMenu, pasteMenu] as MenuItem[];
}
return [pasteMenu] as MenuItem[];
}, [store.nodes]);
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 ( return (
<div <div
style={{ style={{

View File

@ -1,6 +1,6 @@
import { useRef, memo, useEffect, useMemo, useState } from 'react'; import { useRef, memo, useEffect, useMemo, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { NodeResizer, useStore } from '@xyflow/react'; import { NodeResizer, useStore, useReactFlow } from '@xyflow/react';
import { useWallStore } from '../store/wall'; import { useWallStore } from '../store/wall';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -45,13 +45,14 @@ const ShowContent = (props: { data: WallData; selected: boolean }) => {
export const CustomNode = (props: { id: string; data: WallData; selected: boolean }) => { export const CustomNode = (props: { id: string; data: WallData; selected: boolean }) => {
const data = props.data; const data = props.data;
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const selected = props.selected; const reactFlowInstance = useReactFlow();
const zoom = reactFlowInstance.getViewport().zoom;
const wallStore = useWallStore( const wallStore = useWallStore(
useShallow((state) => { useShallow((state) => {
return { return {
setOpen: state.setOpen,
setSelectedNode: state.setSelectedNode, setSelectedNode: state.setSelectedNode,
saveNodes: state.saveNodes, saveNodes: state.saveNodes,
checkAndOpen: state.checkAndOpen,
}; };
}), }),
); );
@ -102,16 +103,17 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
const node = store.getNode(props.id); const node = store.getNode(props.id);
console.log('node eidt', node); console.log('node eidt', node);
if (node) { if (node) {
if (node.data?.noEdit) { const dataType: string = (node?.data?.dataType as string) || '';
message.error('不支持编辑'); if (dataType && dataType?.startsWith('image')) {
message.error('不支持编辑图片');
return; return;
} }
wallStore.setOpen(true); wallStore.checkAndOpen(true, node);
wallStore.setSelectedNode(node);
} else { } else {
message.error('节点不存在'); message.error('节点不存在');
} }
}; };
const handleSize = Math.max(10, 10 / zoom);
return ( return (
<> <>
<div <div
@ -125,7 +127,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
style={style}> style={style}>
<ShowContent data={data} selected={props.selected} /> <ShowContent data={data} selected={props.selected} />
</div> </div>
<div className={clsx('absolute top-0 right-0', props.selected ? 'opacity-100' : 'opacity-0')}> <div className={clsx('absolute top-0 right-0 cursor-pointer', props.selected ? 'opacity-100' : 'opacity-0')}>
<button <button
className='w-6 h-6 flex items-center justify-center' className='w-6 h-6 flex items-center justify-center'
onClick={() => { onClick={() => {
@ -149,6 +151,14 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
if (!heightNum || !widthNum) return; if (!heightNum || !widthNum) return;
store.updateWallRect(props.id, { width: widthNum, height: heightNum }); store.updateWallRect(props.id, { width: widthNum, height: heightNum });
}} }}
handleStyle={
props.selected
? {
width: handleSize,
height: handleSize,
}
: undefined
}
/> />
</> </>
); );

View File

@ -9,7 +9,7 @@ import { message } from '@/modules/message';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { isMac } from '../utils/is-mac'; import { isMac } from '../utils/is-mac';
const Drawer = () => { const Drawer = () => {
const { open, setOpen, selectedNode, setSelectedNode, editValue, setEditValue } = useWallStore( const { open, setOpen, selectedNode, setSelectedNode, editValue, setEditValue, hasEdited, setHasEdited } = useWallStore(
useShallow((state) => ({ useShallow((state) => ({
open: state.open, open: state.open,
setOpen: state.setOpen, setOpen: state.setOpen,
@ -17,18 +17,23 @@ const Drawer = () => {
setSelectedNode: state.setSelectedNode, setSelectedNode: state.setSelectedNode,
editValue: state.editValue, editValue: state.editValue,
setEditValue: state.setEditValue, setEditValue: state.setEditValue,
hasEdited: state.hasEdited,
setHasEdited: state.setHasEdited,
})), })),
); );
const store = useStore((state) => state); const store = useStore((state) => state);
const storeApi = useStoreApi(); const storeApi = useStoreApi();
const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
if (open && selectedNode) { if (open && selectedNode) {
setEditValue(selectedNode?.data.html); setEditValue(selectedNode?.data.html, true);
} }
}, [open, selectedNode]); }, [open, selectedNode]);
useEffect(() => { useEffect(() => {
setMounted(true);
return () => { return () => {
setOpen(false); setOpen(false);
setHasEdited(false);
setSelectedNode(null); setSelectedNode(null);
}; };
}, []); }, []);
@ -52,6 +57,15 @@ const Drawer = () => {
window.removeEventListener('keydown', listener); window.removeEventListener('keydown', listener);
}; };
}, []); }, []);
useEffect(() => {
console.log('editValue', editValue, open, mounted);
if (!open && mounted) {
console.log('hasEdited', hasEdited);
if (hasEdited) {
onSave();
}
}
}, [open, hasEdited, mounted]);
const onSave = () => { const onSave = () => {
const wallStore = useWallStore.getState(); const wallStore = useWallStore.getState();
const selectedNode = wallStore.selectedNode; const selectedNode = wallStore.selectedNode;

View File

@ -3,9 +3,10 @@ import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button, C
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { getNodeData, useWallStore } from '../store/wall'; import { getNodeData, useWallStore } from '../store/wall';
import { useReactFlow, useStore } from '@xyflow/react'; import { useReactFlow, useStore } from '@xyflow/react';
import { useUserWallStore } from '../store/user-wall'; import { useUserWallStore, Wall } from '../store/user-wall';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { WallData } from './CustomNode';
function FormDialog({ open, handleClose, handleSubmit, initialData }) { function FormDialog({ open, handleClose, handleSubmit, initialData }) {
const [data, setData] = useState(initialData || { title: '', description: '', summary: '', tags: [] }); const [data, setData] = useState(initialData || { title: '', description: '', summary: '', tags: [] });
@ -106,7 +107,10 @@ export const SaveModal = () => {
tags: values.tags, tags: values.tags,
markType: 'wallnote' as 'wallnote', markType: 'wallnote' as 'wallnote',
data, data,
}; } as Wall;
if (id) {
fromData.id = id;
}
const loading = message.loading('保存中...'); const loading = message.loading('保存中...');
const res = await userWallStore.saveWall(fromData, { refresh: false }); const res = await userWallStore.saveWall(fromData, { refresh: false });
message.close(loading); message.close(loading);

View File

@ -6,7 +6,7 @@ type User = {
username: string; username: string;
avatar: string; avatar: string;
}; };
type Wall = { export type Wall = {
id?: string; id?: string;
title?: string; title?: string;
description?: string; description?: string;

View File

@ -4,7 +4,10 @@ import { getWallData, setWallData } from '../utils/db';
import { useUserWallStore } from './user-wall'; import { useUserWallStore } from './user-wall';
import { redirectToLogin } from '@/modules/require-to-login'; import { redirectToLogin } from '@/modules/require-to-login';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { randomId } from '../utils/random';
import { DOCS_NODE } from '../docs';
import { toast } from 'react-toastify';
import { SplitButtons } from '../components/SplitToast';
type NodeData<T = { [key: string]: any }> = { type NodeData<T = { [key: string]: any }> = {
id: string; id: string;
position: XYPosition; position: XYPosition;
@ -27,10 +30,13 @@ interface WallState {
saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>; saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>;
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
checkAndOpen: (open?: boolean, data?: any) => void;
selectedNode: NodeData | null; selectedNode: NodeData | null;
setSelectedNode: (node: NodeData | null) => void; setSelectedNode: (node: NodeData | null) => void;
editValue: string; editValue: string;
setEditValue: (value: string) => void; setEditValue: (value: string, init?: boolean) => void;
hasEdited: boolean;
setHasEdited: (hasEdited: boolean) => void;
data?: any; data?: any;
setData: (data: any) => void; setData: (data: any) => void;
init: (id?: string | null) => Promise<void>; init: (id?: string | null) => Promise<void>;
@ -48,17 +54,8 @@ interface WallState {
clear: () => Promise<void>; clear: () => Promise<void>;
exportWall: (nodes: NodeData[]) => Promise<void>; exportWall: (nodes: NodeData[]) => Promise<void>;
clearQueryWall: () => Promise<void>; clearQueryWall: () => Promise<void>;
clearId: () => Promise<void>;
} }
const initialNodes = [
// { id: '1', type: 'wall', position: { x: 0, y: 0 }, data: { html: '1' } },
{
id: '1',
type: 'wall',
position: { x: 0, y: 0 },
data: { html: 'sadfsdaf1 sadfsdaf1 sadfsdaf1 sadfsdaf1 sadfsdaf1 sadfsdaf1 sadfsdaf1 sadfsdaf1', width: 410, height: 212 },
},
// { id: '2', type: 'wall', position: { x: 0, y: 100 }, data: { html: '3332' } },
];
export const useWallStore = create<WallState>((set, get) => ({ export const useWallStore = create<WallState>((set, get) => ({
nodes: [], nodes: [],
@ -69,10 +66,11 @@ export const useWallStore = create<WallState>((set, get) => ({
}, },
saveNodes: async (nodes: NodeData[], opts) => { saveNodes: async (nodes: NodeData[], opts) => {
console.log('nodes', nodes, opts, opts?.showMessage ?? true); console.log('nodes', nodes, opts, opts?.showMessage ?? true);
const showMessage = opts?.showMessage ?? true;
set({ hasEdited: false });
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('保存到本地'); showMessage && message.success('保存到本地');
} else { } else {
const { id } = get(); const { id } = get();
@ -88,19 +86,45 @@ export const useWallStore = create<WallState>((set, get) => ({
}); });
if (res.code === 200) { if (res.code === 200) {
// console.log('saveNodes res', res); // console.log('saveNodes res', res);
message.success('保存成功', { showMessage &&
closeOnClick: true, message.success('保存成功', {
}); closeOnClick: true,
});
} }
} }
} }
}, },
open: false, open: false,
setOpen: (open) => set({ open }), setOpen: (open) => {
set({ open });
},
checkAndOpen: (open, data) => {
const state = get();
if (state.hasEdited || state.open) {
toast(SplitButtons, {
closeButton: false,
className: 'p-0 w-[400px] border border-purple-600/40',
ariaLabel: 'Email received',
onClose: (reason) => {
if (reason === 'success') {
set({ open: true, selectedNode: data, hasEdited: false });
}
},
});
return;
} else set({ open, selectedNode: data });
},
selectedNode: null, selectedNode: null,
setSelectedNode: (node) => set({ selectedNode: node }), setSelectedNode: (node) => set({ selectedNode: node }),
editValue: '', editValue: '',
setEditValue: (value) => set({ editValue: value }), setEditValue: (value, init = false) => {
set({ editValue: value });
if (!init) {
set({ hasEdited: true });
}
},
hasEdited: false,
setHasEdited: (hasEdited) => set({ hasEdited }),
data: null, data: null,
setData: (data) => set({ data }), setData: (data) => set({ data }),
id: null, id: null,
@ -124,7 +148,17 @@ export const useWallStore = create<WallState>((set, get) => ({
redirectToLogin(); redirectToLogin();
} else { } else {
const data = await getWallData(); const data = await getWallData();
set({ nodes: data?.nodes || [], loaded: true }); const nodes = data?.nodes || [];
if (nodes.length === 0) {
set({
nodes: [DOCS_NODE], //
loaded: true,
id: null,
data: null,
});
} else {
set({ nodes, loaded: true, id: null, data: null });
}
} }
}, },
toolbarOpen: false, toolbarOpen: false,
@ -135,7 +169,7 @@ export const useWallStore = create<WallState>((set, get) => ({
setFormDialogData: (data) => set({ formDialogData: data }), setFormDialogData: (data) => set({ formDialogData: data }),
clear: async () => { clear: async () => {
if (get().id) { if (get().id) {
set({ nodes: initialNodes, id: null, selectedNode: null, editValue: '', data: null }); set({ nodes: [], selectedNode: null, editValue: '', data: null });
await useUserWallStore.getState().saveWall({ await useUserWallStore.getState().saveWall({
id: get().id!, id: get().id!,
data: { data: {
@ -143,10 +177,13 @@ export const useWallStore = create<WallState>((set, get) => ({
}, },
}); });
} else { } else {
set({ nodes: initialNodes, id: null, selectedNode: null, editValue: '', data: null }); set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null });
await setWallData({ nodes: [] }); await setWallData({ nodes: [] });
} }
}, },
clearId: async () => {
set({ id: null, data: null });
},
exportWall: async (nodes: NodeData[]) => { exportWall: async (nodes: NodeData[]) => {
const covertData = getNodeData(nodes); const covertData = getNodeData(nodes);
setWallData({ nodes: covertData }); setWallData({ nodes: covertData });
@ -160,6 +197,6 @@ export const useWallStore = create<WallState>((set, get) => ({
a.click(); a.click();
}, },
clearQueryWall: async () => { clearQueryWall: async () => {
set({ nodes: initialNodes, id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false }); set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false });
}, },
})); }));

View File

@ -29,3 +29,49 @@ export const getImageWidthHeightByBase64 = async (
img.src = b64str; img.src = b64str;
}); });
}; };
/**
* width的元素当中
* 使canvasfont-size=16px
* @param str
* @param width
*/
export const getTextWidthHeight = async ({
str,
width,
fontSize = 16,
maxHeight = 600,
minHeight = 100,
}: {
str: string;
width: number;
fontSize?: number;
maxHeight?: number;
minHeight?: number;
}) => {
function calculateTextHeight(text: string, width: number, fontSize: number = 16): number {
// 创建一个隐藏的 DOM 元素来测量文本高度
const element = document.createElement('div');
element.style.position = 'absolute';
element.style.visibility = 'hidden';
element.style.width = `${width}px`;
element.style.fontSize = `${fontSize}px`;
element.style.lineHeight = '1.2'; // 假设行高为 1.2 倍字体大小
element.style.whiteSpace = 'pre-wrap'; // 保留空白并允许换行
element.style.wordWrap = 'break-word'; // 允许长单词换行
element.innerText = text;
document.body.appendChild(element);
const height = element.offsetHeight;
document.body.removeChild(element);
return height;
}
const height = calculateTextHeight(str, width, fontSize);
if (height > maxHeight) {
return { width, height: maxHeight };
} else if (height < minHeight) {
return { width, height: minHeight };
}
return { width, height };
};