diff --git a/package.json b/package.json index cc4af0d..e31b908 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/App.tsx b/src/App.tsx index 0f97a59..826e77c 100644 --- a/src/App.tsx +++ b/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 = () => { } /> } /> + + + + } + /> + + } /> diff --git a/src/modules/layouts/Auth.tsx b/src/modules/layouts/Auth.tsx index b43aa16..d54fa1c 100644 --- a/src/modules/layouts/Auth.tsx +++ b/src/modules/layouts/Auth.tsx @@ -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 && }; + } return <>{}; }; diff --git a/src/pages/wall-share/index.tsx b/src/pages/wall-share/index.tsx new file mode 100644 index 0000000..08803d1 --- /dev/null +++ b/src/pages/wall-share/index.tsx @@ -0,0 +1,13 @@ +import { Routes, Route } from 'react-router-dom'; + +const WallShare = () => { + return
WallShare
; +}; + +export const App = () => { + return ( + + } /> + + ); +}; diff --git a/src/pages/wall/components/SplitToast.tsx b/src/pages/wall/components/SplitToast.tsx new file mode 100644 index 0000000..53806bf --- /dev/null +++ b/src/pages/wall/components/SplitToast.tsx @@ -0,0 +1,24 @@ +import { ToastContentProps } from 'react-toastify'; + +export function SplitButtons({ closeToast }: ToastContentProps) { + return ( + // using a grid with 3 columns +
+
+

提示

+

有未保存的内容,是否继续打开?

+
+ {/* that's the vertical line which separate the text and the buttons*/} +
+
+ {/*specifying a custom closure reason that can be used with the onClose callback*/} + +
+ {/*specifying a custom closure reason that can be used with the onClose callback*/} + +
+
+ ); +} diff --git a/src/pages/wall/docs.ts b/src/pages/wall/docs.ts new file mode 100644 index 0000000..99b11da --- /dev/null +++ b/src/pages/wall/docs.ts @@ -0,0 +1,12 @@ +export const DOCS_NODE = { + id: 'e15owpuh9cv3fgwx5zymtc', + data: { + html: '

Wallnote 基本使用介绍 v0.0.6

可拖拽的随笔记功能。

  • 纯网页界面,数据存储在浏览器(不登陆情况下,只有单个页面)

  • 这个墙随便拖动

  • 双击空格添加一条记录,并打开编辑,esc关闭

  • 富文本编辑器(md语法)

  • 点击节点聚焦后,delete删除

  • 右键空白处粘贴

    • html的内容,编辑会丢失样式

    • 图片的内容(粘贴后不能编辑)

    • 文本内容

    • 复制的节点信息

  • 边框可拖动大小

注意

  • 点击节点聚焦后,如果有滚动条,节点内容才能滚动

  • 图片复制,只能是二进制,文件夹的图片复制后无效。比如snipaste 贴图复制(Can To Do)。

登录后功能

  • 保存而不是临时编辑

TODO

  • do do do

  • ai ++++

', + width: 583, + height: 448, + }, + type: 'wallnote', + position: { x: -901.1464949275596, y: -672.8095405534519 }, + measured: { width: 583, height: 448 }, + selected: true, +}; diff --git a/src/pages/wall/hooks/listen-copy.ts b/src/pages/wall/hooks/listen-copy.ts index c836598..cfb5a8f 100644 --- a/src/pages/wall/hooks/listen-copy.ts +++ b/src/pages/wall/hooks/listen-copy.ts @@ -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(); diff --git a/src/pages/wall/index.tsx b/src/pages/wall/index.tsx index 9dfd916..d156cb0 100644 --- a/src/pages/wall/index.tsx +++ b/src/pages/wall/index.tsx @@ -40,7 +40,15 @@ type NodeData = { export function FlowContent() { const reactFlowInstance = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState([]); - 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}> @@ -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(); }}> 转到首页 diff --git a/src/pages/wall/modules/ContextMenu.tsx b/src/pages/wall/modules/ContextMenu.tsx index 309d979..23a8979 100644 --- a/src/pages/wall/modules/ContextMenu.tsx +++ b/src/pages/wall/modules/ContextMenu.tsx @@ -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 = `图片`; + return this.newNodeData; + } } export const ContextMenu: React.FC = ({ x, y, onClose }) => { const reactFlowInstance = useReactFlow(); @@ -69,71 +139,53 @@ export const ContextMenu: React.FC = ({ x, y, onClose }) => { }; }), ); - // const - const menuList: MenuItem[] = [ - { - label: '粘贴', - icon: , - 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: , + 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: , + 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 (
{ export const CustomNode = (props: { id: string; data: WallData; selected: boolean }) => { const data = props.data; const contentRef = useRef(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 ( <>
-
+