generated from template/vite-react-template
temp: fix bugs
This commit is contained in:
parent
5ef42ee9de
commit
a92e377d9f
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wallnote",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"user": "apps",
|
||||
"scripts": {
|
||||
@ -11,8 +11,8 @@
|
||||
"lint": "eslint .",
|
||||
"deploy": "rsync -avz --delete dist/ light:~/apps/ai/dist",
|
||||
"preview": "vite preview",
|
||||
"prepub": "envision switchOrg apps",
|
||||
"pub": "envision deploy ./dist -k wallnote -v 0.0.3 -y y",
|
||||
"prepub": "pnpm build && envision switch apps",
|
||||
"pub": "envision deploy ./dist -k wallnote -v 0.0.6 -y y",
|
||||
"ev": "npm run build && npm run deploy"
|
||||
},
|
||||
"stackblitz": {
|
||||
|
14
src/App.tsx
14
src/App.tsx
@ -1,5 +1,5 @@
|
||||
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 { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
@ -7,7 +7,7 @@ import { List } from './pages/wall/pages/List';
|
||||
import { Auth } from './modules/layouts/Auth';
|
||||
import { basename } from './modules/basename';
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
|
||||
import { App as WallShareApp } from './pages/wall-share';
|
||||
export const App = () => {
|
||||
return (
|
||||
<>
|
||||
@ -21,6 +21,16 @@ export const App = () => {
|
||||
<Route path='/edit/:id' element={<Flow checkLogin={true} />} />
|
||||
<Route path='/list' element={<List />} />
|
||||
</Route>
|
||||
<Route
|
||||
path='/share/*'
|
||||
element={
|
||||
<Auth auth={false}>
|
||||
<WallShareApp />
|
||||
</Auth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path='*' element={<Navigate to='/' />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<ToastContainer />
|
||||
|
@ -3,7 +3,13 @@ import { useUserWallStore } from '../../pages/wall/store/user-wall';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
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(
|
||||
useShallow((state) => {
|
||||
return { user: state.user, queryMe: state.queryMe };
|
||||
@ -20,5 +26,8 @@ export const Auth = ({ children, auth = true }: { children?: React.ReactNode; au
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (auth) {
|
||||
return <>{userStore.user && <Outlet />}</>;
|
||||
}
|
||||
return <>{<Outlet />}</>;
|
||||
};
|
||||
|
13
src/pages/wall-share/index.tsx
Normal file
13
src/pages/wall-share/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
24
src/pages/wall/components/SplitToast.tsx
Normal file
24
src/pages/wall/components/SplitToast.tsx
Normal 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
12
src/pages/wall/docs.ts
Normal 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,
|
||||
};
|
@ -26,13 +26,12 @@ export const clipboardRead = async () => {
|
||||
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 });
|
||||
const jsonContent = parseIfJson(textPlain);
|
||||
if (jsonContent && jsonContent.type === 'wallnote') {
|
||||
typesDataList.push({ type: 'text/json', data: jsonContent, blob: data });
|
||||
} else {
|
||||
typesDataList.push({ type: 'text/plain', data: textPlain, blob: data });
|
||||
}
|
||||
break;
|
||||
case 'text/html':
|
||||
const textHtml = await data.text();
|
||||
|
@ -40,7 +40,15 @@ type NodeData = {
|
||||
export function FlowContent() {
|
||||
const reactFlowInstance = useReactFlow();
|
||||
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 [mount, setMount] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
@ -65,8 +73,7 @@ export function FlowContent() {
|
||||
};
|
||||
}, [wallStore.nodes]);
|
||||
const onNodeDoubleClick = (event, node) => {
|
||||
wallStore.setOpen(true);
|
||||
wallStore.setSelectedNode(node);
|
||||
wallStore.checkAndOpen(true, node);
|
||||
};
|
||||
const getNewNodes = (showMessage = true) => {
|
||||
const nodes = reactFlowInstance.getNodes();
|
||||
@ -79,7 +86,7 @@ export function FlowContent() {
|
||||
}
|
||||
}, [nodes, mount]);
|
||||
useTabNode();
|
||||
useListenPaster();
|
||||
// useListenPaster();
|
||||
// 添加新节点的函数
|
||||
const onPaneDoubleClick = (event) => {
|
||||
// 计算节点位置
|
||||
@ -97,8 +104,7 @@ export function FlowContent() {
|
||||
return newNodes;
|
||||
});
|
||||
setTimeout(() => {
|
||||
wallStore.setSelectedNode(newNode);
|
||||
wallStore.setOpen(true);
|
||||
wallStore.checkAndOpen(true, newNode);
|
||||
getNewNodes();
|
||||
}, 200);
|
||||
};
|
||||
@ -126,6 +132,8 @@ export function FlowContent() {
|
||||
zoomOnScroll={true}
|
||||
preventScrolling={!hasFoucedNode}
|
||||
onContextMenu={handleContextMenu}
|
||||
minZoom={0.05}
|
||||
maxZoom={20}
|
||||
nodeTypes={CustomNodeType}>
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
@ -149,6 +157,7 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
||||
return {
|
||||
loaded: state.loaded,
|
||||
init: state.init,
|
||||
clearId: state.clearId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -168,6 +177,7 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
wallStore.clearId();
|
||||
}}>
|
||||
转到首页
|
||||
</Button>
|
||||
|
@ -1,24 +1,41 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ToolbarItem, MenuItem } from './toolbar/Toolbar';
|
||||
import { ClipboardPaste } from 'lucide-react';
|
||||
import { ClipboardPaste, Copy } 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';
|
||||
import { getImageWidthHeightByBase64, getTextWidthHeight } from '../utils/get-image-rect';
|
||||
interface ContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
type NewNodeData = {
|
||||
id: string;
|
||||
type: 'wallnote';
|
||||
position: { x: number; y: number };
|
||||
data: {
|
||||
width: number;
|
||||
height: number;
|
||||
html: string;
|
||||
dataType?: string;
|
||||
};
|
||||
};
|
||||
class HasTypeCheck {
|
||||
constructor(list: any[]) {
|
||||
newNodeData: NewNodeData;
|
||||
constructor(list: any[], position: { x: number; y: number }) {
|
||||
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') => {
|
||||
return this.list.some((item) => item.type === type);
|
||||
};
|
||||
@ -30,14 +47,20 @@ class HasTypeCheck {
|
||||
if (hasHtml) {
|
||||
return {
|
||||
code: 200,
|
||||
data: this.getType('text/html')?.data || '',
|
||||
data: {
|
||||
html: this.getType('text/html')?.data || '',
|
||||
dataType: 'text/html',
|
||||
},
|
||||
};
|
||||
}
|
||||
const hasText = this.hasType('text/plain');
|
||||
if (hasText) {
|
||||
return {
|
||||
code: 200,
|
||||
data: this.getType('text/plain')?.data || '',
|
||||
data: {
|
||||
html: this.getType('text/plain')?.data || '',
|
||||
dataType: 'text/plain',
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
@ -57,6 +80,53 @@ class HasTypeCheck {
|
||||
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 }) => {
|
||||
const reactFlowInstance = useReactFlow();
|
||||
@ -69,71 +139,53 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => {
|
||||
};
|
||||
}),
|
||||
);
|
||||
// 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 copyMenu = {
|
||||
label: '复制',
|
||||
icon: <Copy />,
|
||||
key: 'copy',
|
||||
onClick: async () => {
|
||||
const nodes = reactFlowInstance.getNodes();
|
||||
const selectedNode = nodes.find((node) => node.selected);
|
||||
if (!selectedNode) {
|
||||
message.error('没有选中节点');
|
||||
return;
|
||||
}
|
||||
const copyData = JSON.stringify(selectedNode);
|
||||
navigator.clipboard.writeText(copyData);
|
||||
message.success('复制成功');
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1000);
|
||||
},
|
||||
};
|
||||
const pasteMenu = {
|
||||
label: '粘贴',
|
||||
icon: <ClipboardPaste />,
|
||||
key: 'paste',
|
||||
onClick: async () => {
|
||||
const readList = await clipboardRead();
|
||||
const flowPosition = reactFlowInstance.screenToFlowPosition({ x, y });
|
||||
const check = new HasTypeCheck(readList, flowPosition);
|
||||
if (readList.length <= 0) {
|
||||
message.error('粘贴为空');
|
||||
return;
|
||||
}
|
||||
const newNodeData = await check.getData();
|
||||
const nodes = store.nodes;
|
||||
const _nodes = [...nodes, newNodeData];
|
||||
wallStore.setNodes(_nodes);
|
||||
wallStore.saveNodes(_nodes);
|
||||
// reactFlowInstance.setNodes(_nodes);
|
||||
},
|
||||
};
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useRef, memo, useEffect, useMemo, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { NodeResizer, useStore } from '@xyflow/react';
|
||||
import { NodeResizer, useStore, useReactFlow } from '@xyflow/react';
|
||||
import { useWallStore } from '../store/wall';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
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 }) => {
|
||||
const data = props.data;
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const selected = props.selected;
|
||||
const reactFlowInstance = useReactFlow();
|
||||
const zoom = reactFlowInstance.getViewport().zoom;
|
||||
const wallStore = useWallStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
setOpen: state.setOpen,
|
||||
setSelectedNode: state.setSelectedNode,
|
||||
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);
|
||||
console.log('node eidt', node);
|
||||
if (node) {
|
||||
if (node.data?.noEdit) {
|
||||
message.error('不支持编辑');
|
||||
const dataType: string = (node?.data?.dataType as string) || '';
|
||||
if (dataType && dataType?.startsWith('image')) {
|
||||
message.error('不支持编辑图片');
|
||||
return;
|
||||
}
|
||||
wallStore.setOpen(true);
|
||||
wallStore.setSelectedNode(node);
|
||||
wallStore.checkAndOpen(true, node);
|
||||
} else {
|
||||
message.error('节点不存在');
|
||||
}
|
||||
};
|
||||
const handleSize = Math.max(10, 10 / zoom);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -125,7 +127,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
style={style}>
|
||||
<ShowContent data={data} selected={props.selected} />
|
||||
</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
|
||||
className='w-6 h-6 flex items-center justify-center'
|
||||
onClick={() => {
|
||||
@ -149,6 +151,14 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
if (!heightNum || !widthNum) return;
|
||||
store.updateWallRect(props.id, { width: widthNum, height: heightNum });
|
||||
}}
|
||||
handleStyle={
|
||||
props.selected
|
||||
? {
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import { message } from '@/modules/message';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { isMac } from '../utils/is-mac';
|
||||
const Drawer = () => {
|
||||
const { open, setOpen, selectedNode, setSelectedNode, editValue, setEditValue } = useWallStore(
|
||||
const { open, setOpen, selectedNode, setSelectedNode, editValue, setEditValue, hasEdited, setHasEdited } = useWallStore(
|
||||
useShallow((state) => ({
|
||||
open: state.open,
|
||||
setOpen: state.setOpen,
|
||||
@ -17,18 +17,23 @@ const Drawer = () => {
|
||||
setSelectedNode: state.setSelectedNode,
|
||||
editValue: state.editValue,
|
||||
setEditValue: state.setEditValue,
|
||||
hasEdited: state.hasEdited,
|
||||
setHasEdited: state.setHasEdited,
|
||||
})),
|
||||
);
|
||||
const store = useStore((state) => state);
|
||||
const storeApi = useStoreApi();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
if (open && selectedNode) {
|
||||
setEditValue(selectedNode?.data.html);
|
||||
setEditValue(selectedNode?.data.html, true);
|
||||
}
|
||||
}, [open, selectedNode]);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => {
|
||||
setOpen(false);
|
||||
setHasEdited(false);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
}, []);
|
||||
@ -52,6 +57,15 @@ const Drawer = () => {
|
||||
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 wallStore = useWallStore.getState();
|
||||
const selectedNode = wallStore.selectedNode;
|
||||
|
@ -3,9 +3,10 @@ import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button, C
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { getNodeData, useWallStore } from '../store/wall';
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
import { WallData } from './CustomNode';
|
||||
|
||||
function FormDialog({ open, handleClose, handleSubmit, initialData }) {
|
||||
const [data, setData] = useState(initialData || { title: '', description: '', summary: '', tags: [] });
|
||||
@ -106,7 +107,10 @@ export const SaveModal = () => {
|
||||
tags: values.tags,
|
||||
markType: 'wallnote' as 'wallnote',
|
||||
data,
|
||||
};
|
||||
} as Wall;
|
||||
if (id) {
|
||||
fromData.id = id;
|
||||
}
|
||||
const loading = message.loading('保存中...');
|
||||
const res = await userWallStore.saveWall(fromData, { refresh: false });
|
||||
message.close(loading);
|
||||
|
@ -6,7 +6,7 @@ type User = {
|
||||
username: string;
|
||||
avatar: string;
|
||||
};
|
||||
type Wall = {
|
||||
export type Wall = {
|
||||
id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
@ -4,7 +4,10 @@ import { getWallData, setWallData } from '../utils/db';
|
||||
import { useUserWallStore } from './user-wall';
|
||||
import { redirectToLogin } from '@/modules/require-to-login';
|
||||
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 }> = {
|
||||
id: string;
|
||||
position: XYPosition;
|
||||
@ -27,10 +30,13 @@ interface WallState {
|
||||
saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
checkAndOpen: (open?: boolean, data?: any) => void;
|
||||
selectedNode: NodeData | null;
|
||||
setSelectedNode: (node: NodeData | null) => void;
|
||||
editValue: string;
|
||||
setEditValue: (value: string) => void;
|
||||
setEditValue: (value: string, init?: boolean) => void;
|
||||
hasEdited: boolean;
|
||||
setHasEdited: (hasEdited: boolean) => void;
|
||||
data?: any;
|
||||
setData: (data: any) => void;
|
||||
init: (id?: string | null) => Promise<void>;
|
||||
@ -48,17 +54,8 @@ interface WallState {
|
||||
clear: () => Promise<void>;
|
||||
exportWall: (nodes: NodeData[]) => 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) => ({
|
||||
nodes: [],
|
||||
@ -69,10 +66,11 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
},
|
||||
saveNodes: async (nodes: NodeData[], opts) => {
|
||||
console.log('nodes', nodes, opts, opts?.showMessage ?? true);
|
||||
const showMessage = opts?.showMessage ?? true;
|
||||
set({ hasEdited: false });
|
||||
if (!get().id) {
|
||||
const covertData = getNodeData(nodes);
|
||||
setWallData({ nodes: covertData });
|
||||
const showMessage = opts?.showMessage ?? true;
|
||||
showMessage && message.success('保存到本地');
|
||||
} else {
|
||||
const { id } = get();
|
||||
@ -88,19 +86,45 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
});
|
||||
if (res.code === 200) {
|
||||
// console.log('saveNodes res', res);
|
||||
message.success('保存成功', {
|
||||
closeOnClick: true,
|
||||
});
|
||||
showMessage &&
|
||||
message.success('保存成功', {
|
||||
closeOnClick: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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,
|
||||
setSelectedNode: (node) => set({ selectedNode: node }),
|
||||
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,
|
||||
setData: (data) => set({ data }),
|
||||
id: null,
|
||||
@ -124,7 +148,17 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
redirectToLogin();
|
||||
} else {
|
||||
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,
|
||||
@ -135,7 +169,7 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
setFormDialogData: (data) => set({ formDialogData: data }),
|
||||
clear: async () => {
|
||||
if (get().id) {
|
||||
set({ nodes: initialNodes, id: null, selectedNode: null, editValue: '', data: null });
|
||||
set({ nodes: [], selectedNode: null, editValue: '', data: null });
|
||||
await useUserWallStore.getState().saveWall({
|
||||
id: get().id!,
|
||||
data: {
|
||||
@ -143,10 +177,13 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
set({ nodes: initialNodes, id: null, selectedNode: null, editValue: '', data: null });
|
||||
set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null });
|
||||
await setWallData({ nodes: [] });
|
||||
}
|
||||
},
|
||||
clearId: async () => {
|
||||
set({ id: null, data: null });
|
||||
},
|
||||
exportWall: async (nodes: NodeData[]) => {
|
||||
const covertData = getNodeData(nodes);
|
||||
setWallData({ nodes: covertData });
|
||||
@ -160,6 +197,6 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
a.click();
|
||||
},
|
||||
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 });
|
||||
},
|
||||
}));
|
||||
|
@ -29,3 +29,49 @@ export const getImageWidthHeightByBase64 = async (
|
||||
img.src = b64str;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 我有一个字符串,在在宽度为width的元素当中,自动换行,需要知道最后有多少行。
|
||||
* 不使用canvas,通过文本font-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 };
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user