diff --git a/src/index.css b/src/index.css index 4de0fb8..834fff3 100644 --- a/src/index.css +++ b/src/index.css @@ -34,7 +34,8 @@ body { } } -.markdown-body,.tiptap { +.markdown-body, +.tiptap { ul, li { list-style: unset; @@ -47,4 +48,10 @@ iframe { border: unset; width: 100%; height: 100%; + /* will-change: transform; */ + /* pointer-events: none; */ + position: fixed; + top: 0; + left: 0; + z-index: 99999; } diff --git a/src/modules/panels/components/WindowManager.tsx b/src/modules/panels/components/WindowManager.tsx index 08454f7..aa8813d 100644 --- a/src/modules/panels/components/WindowManager.tsx +++ b/src/modules/panels/components/WindowManager.tsx @@ -1,5 +1,5 @@ -import React, { useState, useCallback, useRef, useEffect, RefObject } from 'react'; -import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown, CommandIcon } from 'lucide-react'; +import React, { useState, useCallback, useRef, useEffect, RefObject, useMemo } from 'react'; +import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown, CommandIcon, LogOut } from 'lucide-react'; import { WindowData, WindowPosition } from '../types'; import classNames from 'clsx'; import Draggable from 'react-draggable'; @@ -273,8 +273,23 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar = // window.removeEventListener('resize', handleResize); // }; // }, []); + const showLogout = useMemo(() => { + return localStorage.getItem('token'); + }, []); return (
+ {showLogout && ( +
{ + context?.app?.call?.({ + path: 'user', + key: 'logout', + }); + }}> + +
+ )}
{ diff --git a/src/modules/panels/render/manager/manager.ts b/src/modules/panels/render/manager/manager.ts index 3843292..57b8954 100644 --- a/src/modules/panels/render/manager/manager.ts +++ b/src/modules/panels/render/manager/manager.ts @@ -38,6 +38,8 @@ export class BaseRender { // @ts-ignore const app = (await useContextKey('app')) as QueryRouterServer; const render = windowData.render; + console.log('base render', render, render?.command); + if (render?.command) { const res = await app.call({ path: render.command.path, @@ -65,6 +67,8 @@ export class BaseRender { data: windowData, }); } + } else { + console.log('render error', res); } } } diff --git a/src/modules/panels/store/create/create-editor-window.ts b/src/modules/panels/store/create/create-editor-window.ts index a479c88..b2aaf35 100644 --- a/src/modules/panels/store/create/create-editor-window.ts +++ b/src/modules/panels/store/create/create-editor-window.ts @@ -43,7 +43,7 @@ export const createEditorWindow = (pageId: string, nodeData: any, windowData?: W render: { command: { path: 'editor', - key: 'render', + key: 'nodeRender', payload: { pageId: pageId, id: nodeData.id, diff --git a/src/modules/panels/store/index.ts b/src/modules/panels/store/index.ts index e03cd99..66e07df 100644 --- a/src/modules/panels/store/index.ts +++ b/src/modules/panels/store/index.ts @@ -90,7 +90,8 @@ export const usePanelStore = create((set, get) => ({ set({ data: { - windows: [e.windowData], + // windows: [e.windowData], + windows: [], showTaskbar: true, }, }); @@ -136,8 +137,9 @@ export const usePanelStore = create((set, get) => ({ const { width, height } = getDocumentWidthAndHeight(); data.windows.push({ id: '__ai__', - title: 'AI Command', + title: '🤖 AI Command', type: 'command', + showTitle: true, position: { x: 100, y: height - 200 - 40, @@ -147,6 +149,15 @@ export const usePanelStore = create((set, get) => ({ }, resizeHandles: ['se', 'sw', 'ne', 'nw', 's', 'w', 'n', 'e'], show: true, + render: { + command: { + path: 'editor', + key: 'render', + payload: { + id: '__ai__', + }, + }, + }, }); } // set({ data: { ...data, windows: data.windows } }); @@ -166,20 +177,20 @@ export const usePanelStore = create((set, get) => ({ }, })); -const e = createEditorWindow( - '123', - { - id: '123', - title: '123', - type: 'editor', - position: { x: 0, y: 0, width: 100, height: 100, zIndex: 1000 }, - }, - createDemoEditorWindow({ - id: '123', - title: '123', - type: 'editor', - position: { x: 0, y: 0, width: 100, height: 100, zIndex: 1000 }, - }), -); +// const e = createEditorWindow( +// '123', +// { +// id: '123', +// title: '123', +// type: 'editor', +// position: { x: 0, y: 0, width: 100, height: 100, zIndex: 1000 }, +// }, +// createDemoEditorWindow({ +// id: '123', +// title: '123', +// type: 'editor', +// position: { x: 0, y: 0, width: 100, height: 100, zIndex: 1000 }, +// }), +// ); -console.log('e', e); +// console.log('e', e); diff --git a/src/pages/demo-login/index.css b/src/pages/demo-login/index.css new file mode 100644 index 0000000..dbc7738 --- /dev/null +++ b/src/pages/demo-login/index.css @@ -0,0 +1,40 @@ + +.demo-login-prompt { + background-color: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; + width: 90%; +} + +.demo-login-link { + margin-bottom: 1rem; + font-size: 1.2rem; +} + +.demo-login-link a { + color: #4a90e2; + text-decoration: none; + transition: color 0.3s ease; +} + +.demo-login-link a:hover { + color: #357abd; + text-decoration: underline; +} + +.demo-accounts { + color: #666; + font-size: 0.95rem; +} + +.demo-account { + background-color: #f0f0f0; + padding: 0.2rem 0.5rem; + border-radius: 4px; + margin: 0 0.3rem; + color: #333; + font-family: monospace; +} \ No newline at end of file diff --git a/src/pages/demo-login/index.tsx b/src/pages/demo-login/index.tsx new file mode 100644 index 0000000..f96ae7c --- /dev/null +++ b/src/pages/demo-login/index.tsx @@ -0,0 +1,27 @@ +import { Button } from '@mui/material'; +import './index.css'; +export const AppendDemo = () => { + return ( +
+

+ 请先登录 +

+
+ ); +}; + +export const DemoLogin = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/pages/editor/NodeTextEditor.tsx b/src/pages/editor/NodeTextEditor.tsx index 4025a7c..4666591 100644 --- a/src/pages/editor/NodeTextEditor.tsx +++ b/src/pages/editor/NodeTextEditor.tsx @@ -30,6 +30,11 @@ export const useListenCtrlS = (saveContent: () => void, exitEdit: () => void) => type EditorProps = { id?: string; }; +/** + * Node Edit Editor + * @param param0 + * @returns + */ export const NodeTextEditor = ({ id }: EditorProps) => { const textEditorRef = useRef(null); const editorRef = useRef(null); @@ -61,7 +66,7 @@ export const NodeTextEditor = ({ id }: EditorProps) => { }; const exitEdit = () => { // 退出编辑 - saveContent() + saveContent(); setTimeout(() => { app.call({ path: 'panels', diff --git a/src/pages/editor/index.tsx b/src/pages/editor/index.tsx index 1607613..579be14 100644 --- a/src/pages/editor/index.tsx +++ b/src/pages/editor/index.tsx @@ -1,2 +1,62 @@ -import { Editor } from '@/modules/editor'; -export { Editor }; +import { TextEditor } from '@/modules/tiptap/editor'; +import { useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; + +export const useListenCtrlEnter = (callback: () => void) => { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.key === 'Enter') { + event.preventDefault(); + callback(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); +}; +type EditorProps = { + className?: string; + value?: string; + id?: string; + onChange?: (value: string) => void; +}; +export const AiEditor = ({ className, value, onChange, id }: EditorProps) => { + const textEditorRef = useRef(null); + const editorRef = useRef(null); + const [mount, setMount] = useState(false); + useEffect(() => { + const editor = new TextEditor(); + textEditorRef.current = editor; + editor.createEditor(editorRef.current!, { + html: value, + onUpdateHtml: (html) => { + onChange?.(html); + }, + }); + setMount(true); + return () => { + editor.destroy(); + }; + }, []); + useListenCtrlEnter(() => { + context?.app.call({ + path: 'command', + key: 'handle', + payload: { + html: textEditorRef.current?.getHtml() || '', + }, + }); + }); + useEffect(() => { + if (textEditorRef.current && id && mount) { + textEditorRef.current.setContent(value || ''); + } + }, [id, mount]); + return ( +
+
+
+ ); +}; diff --git a/src/pages/wall/components/SplitToast.tsx b/src/pages/wall/components/SplitToast.tsx index 53806bf..5f9fbb6 100644 --- a/src/pages/wall/components/SplitToast.tsx +++ b/src/pages/wall/components/SplitToast.tsx @@ -22,3 +22,14 @@ export function SplitButtons({ closeToast }: ToastContentProps) {
); } + +// 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 }); +// } +// }, +// }); diff --git a/src/pages/wall/constants.ts b/src/pages/wall/constants.ts index 0667cd3..b49ca59 100644 --- a/src/pages/wall/constants.ts +++ b/src/pages/wall/constants.ts @@ -1 +1,10 @@ export const BlankNoteText = 'double click to edit'; + +// https://www.reactbits.dev/text-animations/circular-text +export const CircularText = + ''; + +export const CircularText2 = + ''; + +export const CircularText3 = ``; diff --git a/src/pages/wall/hooks/tab-node.ts b/src/pages/wall/hooks/tab-node.ts index 068ce6f..7b580f1 100644 --- a/src/pages/wall/hooks/tab-node.ts +++ b/src/pages/wall/hooks/tab-node.ts @@ -4,10 +4,10 @@ import { useWallStore } from '../store/wall'; import { useShallow } from 'zustand/react/shallow'; export const useTabNode = () => { const reactFlowInstance = useReactFlow(); - const open = useWallStore(useShallow((state) => state.open)); useEffect(() => { - if (open) return; const listener = (event: any) => { + const selected = reactFlowInstance.getNodes().find((node) => node.selected); + if (!selected) return; if (event.key === 'Tab') { const nodes = reactFlowInstance.getNodes(); const selectedNode = nodes.find((node) => node.selected); @@ -56,9 +56,19 @@ export const useTabNode = () => { event.stopPropagation(); } }; + const rightClickListener = (event: any) => { + const selected = reactFlowInstance.getNodes().find((node) => node.selected); + if (!selected) return; + if (event.button === 2) { + event.preventDefault(); + event.stopPropagation(); + } + }; window.addEventListener('keydown', listener); + window.addEventListener('contextmenu', rightClickListener); return () => { window.removeEventListener('keydown', listener); + window.removeEventListener('contextmenu', rightClickListener); }; - }, [reactFlowInstance, open]); + }, [reactFlowInstance]); }; diff --git a/src/pages/wall/index.tsx b/src/pages/wall/index.tsx index f7a015e..bdba154 100644 --- a/src/pages/wall/index.tsx +++ b/src/pages/wall/index.tsx @@ -20,7 +20,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCheckDoubleClick } from './hooks/check-double-click'; import { randomId } from './utils/random'; import { CustomNodeType } from './modules/CustomNode'; -import Drawer from './modules/Drawer'; import { message } from '@/modules/message'; import { useShallow } from 'zustand/react/shallow'; import { BlankNoteText } from './constants'; @@ -33,6 +32,7 @@ import { useListenPaster } from './hooks/listen-copy'; import { ContextMenu } from './modules/ContextMenu'; import { useSelect } from './hooks/use-select'; import clsx from 'clsx'; +import { AppendDemo, DemoLogin } from '../demo-login'; type NodeData = { id: string; position: XYPosition; @@ -47,6 +47,7 @@ export function FlowContent() { return { nodes: state.nodes, saveNodes: state.saveNodes, + saveDataNode: state.saveDataNode, checkAndOpen: state.checkAndOpen, mouseSelect: state.mouseSelect, // 鼠标模式,不能拖动 setMouseSelect: state.setMouseSelect, @@ -69,7 +70,7 @@ export function FlowContent() { wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id)); } if (change.type === 'position' && change.dragging === false) { - getNewNodes(false); + getNewNodes(false, changes); } onNodesChange(changes); }, []); @@ -96,9 +97,15 @@ export function FlowContent() { const onNodeDoubleClick = (event, node) => { wallStore.checkAndOpen(true, node); }; - const getNewNodes = (showMessage = true) => { + const getNewNodes = (showMessage = true, changes?: NodeChange[]) => { const nodes = reactFlowInstance.getNodes(); - wallStore.saveNodes(nodes, { showMessage: showMessage }); + // wallStore.saveNodes(nodes, { showMessage: showMessage }); + // console.log('change', changes); + const operateNodes = nodes.filter((node) => { + return changes?.some((change) => change.type === 'position' && change.id === node.id); + }); + console.log('operateNodes', operateNodes); + wallStore.saveDataNode(operateNodes); }; useEffect(() => { if (mount) { @@ -125,7 +132,8 @@ export function FlowContent() { }); setTimeout(() => { wallStore.checkAndOpen(true, newNode); - getNewNodes(); + // getNewNodes(); + wallStore.saveDataNode([newNode]); }, 200); }; const hasFoucedNode = useMemo(() => { @@ -187,11 +195,10 @@ export function FlowContent() { - {contextMenu && } - {' '} + {isSelecting && selectionBox && (
); } -export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { - // const { id } = useParams(); - const id = ''; - // const navigate = useNavigate(); +export const Flow = ({ id }: { checkLogin?: boolean; id?: string }) => { + const token = localStorage.getItem('token'); + if (!token) { + return ; + } const wallStore = useWallStore( useShallow((state) => { return { @@ -225,23 +233,14 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { useEffect(() => { wallStore.init(id); - console.log('checkLogin', checkLogin, id); - }, [id, checkLogin]); + }, [id]); if (!wallStore.loaded) { return
loading...
; } else if (wallStore.loaded === 'error') { return (
-
获取失败,请稍后刷新重试,或者转到首页
- +
获取失败,请稍后刷新重试
); } @@ -251,13 +250,3 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { ); }; -export const FlowStatus = () => { - const { nodes } = useWallStore(); - const reactFlow = useReactFlow(); - const flowStore = useStore((state) => state); - return ( -
-
节点数量: {nodes.length}
-
- ); -}; diff --git a/src/pages/wall/modules/CustomNode.css b/src/pages/wall/modules/CustomNode.css index 9805b2b..b09d3da 100644 --- a/src/pages/wall/modules/CustomNode.css +++ b/src/pages/wall/modules/CustomNode.css @@ -1,5 +1,7 @@ @import 'tailwindcss'; - +:root { + --xy-resize-background-color: #000; +} @layer components { .node-editor { @apply w-full h-full bg-white; @@ -27,10 +29,10 @@ :root { --purple-light: #e0e0ff; /* 默认浅紫色背景 */ - --black: #000000; /* 默认黑色 */ - --white: #ffffff; /* 默认白色 */ - --gray-3: #d3d3d3; /* 默认灰色3 */ - --gray-2: #e5e5e5; /* 默认灰色2 */ + --black: #000000; /* 默认黑色 */ + --white: #ffffff; /* 默认白色 */ + --gray-3: #d3d3d3; /* 默认灰色3 */ + --gray-2: #e5e5e5; /* 默认灰色2 */ } .tiptap-preview { .tiptap { @@ -80,7 +82,7 @@ .tiptap h2 { /* margin-top: 3.5rem; */ margin-top: 1rem; - margin-bottom: .5rem; + margin-bottom: 0.5rem; } .tiptap h1 { @@ -131,7 +133,7 @@ } .tiptap mark { - background-color: #FAF594; + background-color: #faf594; border-radius: 0.4rem; box-decoration-break: clone; padding: 0.1rem 0.3rem; diff --git a/src/pages/wall/modules/CustomNode.tsx b/src/pages/wall/modules/CustomNode.tsx index fc17ab3..521afc0 100644 --- a/src/pages/wall/modules/CustomNode.tsx +++ b/src/pages/wall/modules/CustomNode.tsx @@ -12,13 +12,10 @@ export type WallData> = { html: string; width?: number; height?: number; + updatedAt?: number; [key: string]: any; } & T; -const ShowContent = (props: { data: WallData; selected: boolean }) => { - const html = props.data.html; - const selected = props.selected; - const showRef = useRef(null); - if (!html) return
; +const ShowContent = (props: { data: WallData; id: string; selected: boolean }) => { const [highlightHtml, setHighlightHtml] = useState(''); const highlight = async (html: string) => { const _html = html.replace(/
([\s\S]*?)<\/code><\/pre>/g, (match, p1, p2) => {
@@ -27,19 +24,23 @@ const ShowContent = (props: { data: WallData; selected: boolean }) => {
     return _html;
   };
   useEffect(() => {
-    highlight(html).then((res) => {
+    highlight(props.data.html).then((res) => {
       setHighlightHtml(res);
     });
-  }, [html]);
-
+  }, [props.data.html]);
+  useEffect(() => {
+    const id = props.id;
+    const container = document.querySelector('.id' + id);
+    if (container) {
+      container.innerHTML = highlightHtml;
+    }
+  }, [highlightHtml, props.data.updatedAt]);
   return (
     
+ pointerEvents: props.selected ? 'auto' : 'none', + }}>
); }; @@ -52,7 +53,6 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea useShallow((state) => { return { id: state.id, - setSelectedNode: state.setSelectedNode, saveNodes: state.saveNodes, checkAndOpen: state.checkAndOpen, }; @@ -87,9 +87,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea }); const width = data.width || 100; const height = data.height || 100; - const style: React.CSSProperties = {}; - style.width = width; - style.height = height; + const showOpen = () => { const node = store.getNode(props.id); console.log('node eidt', node); @@ -104,33 +102,33 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea }, }, }); - // if (node) { - // const dataType: string = (node?.data?.dataType as string) || ''; - // if (dataType && dataType?.startsWith('image')) { - // message.error('不支持编辑图片'); - // return; - // } else if (dataType) { - // message.error('不支持编辑'); - // return; - // } - // wallStore.checkAndOpen(true, node); - // } else { - // message.error('节点不存在'); - // } }; - const handleSize = Math.max(10, 10 / zoom); + const handleSize = Math.max(8, 8 / zoom); return ( <> +
{ showOpen(); - // e.stopPropagation(); e.preventDefault(); }} - className={clsx('w-full h-full border relative border-gray-300 min-w-[100px] min-h-[50px] tiptap-preview')} - style={style}> - + className={clsx('w-full h-full border relative border-gray-300 min-w-[100px] min-h-[50px] tiptap-preview', { + 'pointer-events-none': !props.selected, + 'pointer-events-auto': props.selected, + })} + style={{ + width: width, + height: height, + }}> +
- {selectedNode && ( -
- -
- )} -
-
- {selectedNode && open && } -
-
- ); -}; - -export default Drawer; diff --git a/src/pages/wall/modules/FormDialog.tsx b/src/pages/wall/modules/FormDialog.tsx index 1c63972..8960144 100644 --- a/src/pages/wall/modules/FormDialog.tsx +++ b/src/pages/wall/modules/FormDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button, Chip } from '@mui/material'; import { useShallow } from 'zustand/react/shallow'; import { getNodeData, useWallStore } from '../store/wall'; @@ -94,44 +94,51 @@ export const SaveModal = () => { const { showFormDialog, setShowFormDialog, formDialogData, setFormDialogData } = wallStore; const reactFlowInstance = useReactFlow(); // const navigate = useNavigate(); - const { id } = wallStore; - const onSubmit = async (values) => { - const nodes = reactFlowInstance.getNodes(); - const data = { - nodes: getNodeData(nodes), - }; - const fromData = { - title: values.title, - description: values.description, - summary: values.summary, - 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); - if (res.code === 200) { - setShowFormDialog(false); + const onSubmit = useCallback( + async (values) => { + const { id } = wallStore; if (!id) { - // 新创建 - const data = res.data; - message.info('redirect to edit page'); - wallStore.clear(); - setTimeout(() => { - // navigate(`/edit/${data.id}`); - }, 2000); - } else { - // 编辑 - wallStore.setData(res.data); + message.error('请先保存到账号'); + return; } - } else { - message.error('保存失败'); - } - }; + const nodes = reactFlowInstance.getNodes(); + const data = { + nodes: getNodeData(nodes), + }; + const fromData = { + title: values.title, + description: values.description, + summary: values.summary, + 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); + if (res.code === 200) { + setShowFormDialog(false); + if (!id) { + // 新创建 + const data = res.data; + message.info('redirect to edit page'); + wallStore.clear(); + setTimeout(() => { + // navigate(`/edit/${data.id}`); + }, 2000); + } else { + // 编辑 + wallStore.setData(res.data); + } + } else { + message.error('保存失败'); + } + }, + [reactFlowInstance, wallStore.id], + ); if (!showFormDialog) { return null; } diff --git a/src/pages/wall/modules/toolbar/Toolbar.tsx b/src/pages/wall/modules/toolbar/Toolbar.tsx index e8a8e0d..2bdac48 100644 --- a/src/pages/wall/modules/toolbar/Toolbar.tsx +++ b/src/pages/wall/modules/toolbar/Toolbar.tsx @@ -130,115 +130,42 @@ export const ToolbarContent = ({ open }) => { } }, }, - { - label: '清空', - key: 'clear', - icon: , - onClick: async () => { - await wallStore.clear(); - message.success('清空成功'); - store.setNodes([]); - }, - }, ]; - if (hasLogin) { - menuList.unshift({ - label: '我的笔记', - key: 'myWall', - icon: , - onClick: () => { - // - }, - }); - } - if (!hasLogin) { - menuList.push({ - label: '登录', - key: 'login', - icon: , - onClick: () => { - redirectToLogin(); - }, - }); - if (wallStore.id) { - menuList.push({ - label: '删除', - key: 'delete', - icon: , - onClick: async () => { - const res = await userWallStore.deleteWall(wallStore.id!); - if (res.code === 200) { - // navigate('/'); - } - }, - }); - } - } else { - if (!wallStore.id) { - menuList.push({ - label: '保存到账号', - key: 'saveToAccount', - icon: , - onClick: () => { - wallStore.setShowFormDialog(true); - wallStore.setFormDialogData({ - title: '', - description: '', - tags: [], - summary: '', - }); - }, - }); - } else { - menuList.push({ - label: '编辑信息', - key: 'saveToAccount', - icon: , - onClick: () => { - wallStore.setShowFormDialog(true); - const data = wallStore.data; - wallStore.setFormDialogData({ - title: data?.title, - description: data?.description, - tags: data?.tags, - summary: data?.summary, - }); - }, - }); - menuList.push({ - label: '新增', - key: 'add', - icon: , - onClick: () => { - // navigate(`/`); - wallStore.clearQueryWall(); - }, - }); - menuList.push({ - label: '删除', - key: 'delete', - icon: , - className: 'text-red-500', - onClick: async () => { - const res = await userWallStore.deleteWall(wallStore.id!); - if (res.code === 200) { - message.success('删除成功,返回首页'); - wallStore.clearQueryWall(); - // navigate('/'); - } - }, - }); - } + menuList.push({ + label: '删除', + key: 'delete', + icon: , + onClick: async () => { + const res = await userWallStore.deleteWall(wallStore.id!); + if (res.code === 200) { + // navigate('/'); + } + }, + }); - menuList.push({ - label: '退出 ', - key: 'logout', - icon: , - onClick: () => { - userWallStore.logout(); - }, - }); - } + menuList.push({ + label: '编辑信息', + key: 'saveToAccount', + icon: , + onClick: () => { + wallStore.setShowFormDialog(true); + const data = wallStore.data; + wallStore.setFormDialogData({ + title: data?.title, + description: data?.description, + tags: data?.tags, + summary: data?.summary, + }); + }, + }); + menuList.push({ + label: '退出 ', + key: 'logout', + icon: , + onClick: () => { + userWallStore.logout(); + }, + }); return ( wallStore.setToolbarOpen(false)}>
diff --git a/src/pages/wall/store/user-wall.ts b/src/pages/wall/store/user-wall.ts index 800937b..6da4253 100644 --- a/src/pages/wall/store/user-wall.ts +++ b/src/pages/wall/store/user-wall.ts @@ -30,8 +30,11 @@ interface UserWallStore { wallList: Wall[]; queryWallList: () => Promise; logout: () => void; - saveWall: (data: Wall, opts?: { refresh?: boolean, showMessage?: boolean }) => Promise; - queryWall: (id: string) => Promise; + saveWall: (data: Wall, opts?: { refresh?: boolean; showMessage?: boolean }) => Promise; + saveOneNode: (id: string, node: any) => Promise; + saveDataNodes: (id: string, nodes: any[], opts?: { showMessage?: boolean }) => Promise; + queryWall: (id?: string) => Promise; + queryWallVersion: (id?: string) => Promise; deleteWall: (id: string) => Promise; } @@ -66,7 +69,7 @@ export const useUserWallStore = create((set, get) => ({ set({ wallList: res.data.list }); } }, - saveWall: async (data: Wall, opts?: { refresh?: boolean, showMessage?: boolean }) => { + saveWall: async (data: Wall, opts?: { refresh?: boolean; showMessage?: boolean }) => { const { queryWallList } = get(); const res = await query.post({ path: 'mark', @@ -81,7 +84,27 @@ export const useUserWallStore = create((set, get) => ({ } return res; }, - queryWall: async (id: string) => { + saveOneNode: async (id: string, node: any) => { + const res = await query.post({ + path: 'mark', + key: 'updateNode', + data: { id, node }, + }); + return res; + }, + saveDataNodes: async (id: string, nodeOperateList: any[], opts?: { showMessage?: boolean }) => { + const res = await query.post({ + path: 'mark', + key: 'updateNodes', + data: { id, nodeOperateList }, + }); + if (res.code === 200) { + opts?.showMessage && message.success('保存成功'); + return res; + } + return res; + }, + queryWall: async (id?: string) => { const res = await query.post({ path: 'mark', key: 'get', @@ -97,6 +120,14 @@ export const useUserWallStore = create((set, get) => ({ }); return res; }, + queryWallVersion: async (id?: string) => { + const res = await query.post({ + path: 'mark', + key: 'getVersion', + id, + }); + return res; + }, logout: () => { set({ user: undefined }); localStorage.removeItem('token'); diff --git a/src/pages/wall/store/wall.ts b/src/pages/wall/store/wall.ts index 02ba8a1..f0f6bef 100644 --- a/src/pages/wall/store/wall.ts +++ b/src/pages/wall/store/wall.ts @@ -1,13 +1,12 @@ -import { create } from 'zustand'; +import { create, StateCreator, StoreApi, UseBoundStore } from 'zustand'; import { XYPosition } from '@xyflow/react'; -import { getWallData, setWallData } from '../utils/db'; +import { getCacheWallData, setCacheWallData } 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'; +import { useContextKey } from '@kevisual/system-lib/dist/web-config'; + type NodeData = { id: string; position: XYPosition; @@ -27,23 +26,14 @@ interface WallState { // 只做传递 nodes: NodeData[]; setNodes: (nodes: NodeData[]) => void; + saveDataNode: (nodes: NodeData[]) => Promise; saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise; - 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, init?: boolean) => void; - hasEdited: boolean; - setHasEdited: (hasEdited: boolean) => void; data?: any; setData: (data: any) => void; - init: (id?: string | null) => Promise; + init: (id?: string) => Promise; id: string | null; setId: (id: string | null) => void; - loading: boolean; - setLoading: (loading: boolean) => void; loaded: boolean | 'error'; toolbarOpen: boolean; setToolbarOpen: (open: boolean) => void; @@ -60,25 +50,44 @@ interface WallState { getNodeById: (id: string) => Promise; saveNodeById: (id: string, data: any) => Promise; } - -export const useWallStore = create((set, get) => ({ - nodes: [], - loading: false, - setLoading: (loading) => set({ loading }), - setNodes: (nodes) => { - set({ nodes }); - }, - saveNodes: async (nodes: NodeData[], opts) => { - const showMessage = opts?.showMessage ?? true; - set({ hasEdited: false }); - if (!get().id) { - const covertData = getNodeData(nodes); - setWallData({ nodes: covertData }); - } else { - const { id } = get(); - const userWallStore = useUserWallStore.getState(); - if (id) { +export class WallStore { + private storeMap: Map>> = new Map(); + constructor() { + this.crateStoreById('today'); + } + crateStoreById(id: string) { + const store = create((set, get) => ({ + nodes: [], + setNodes: (nodes) => { + set({ nodes }); + }, + saveDataNode: async (nodes: NodeData[]) => { + const id = get().id; + if (!id) { + message.error('没有id'); + return; + } const covertData = getNodeData(nodes); + const nodeOperateList = covertData.map((item) => ({ + node: item, + })); + const res = await useUserWallStore.getState().saveDataNodes(id, nodeOperateList); + + if (res.code === 200) { + message.success('保存成功'); + } else { + message.error('保存失败'); + } + }, + saveNodes: async (nodes: NodeData[], opts) => { + const showMessage = opts?.showMessage ?? true; + const id = get().id; + if (!id) { + message.error('没有id'); + return; + } + const covertData = getNodeData(nodes); + const userWallStore = useUserWallStore.getState(); const res = await userWallStore.saveWall({ id, data: { @@ -91,139 +100,140 @@ export const useWallStore = create((set, get) => ({ message.success('保存成功', { closeOnClick: true, }); + const markRes = res.data; + setCacheWallData(markRes, markRes?.id); } - } - } - }, - open: false, - 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 }); + }, + checkAndOpen: (open, data) => { + // + }, + data: null, + setData: (data) => set({ data }), + id: null, + setId: (id) => set({ id }), + loaded: false, + init: async (id?: string) => { + // 如果登陆了且如果有id,从服务器获取 + // 没有id,获取缓存 + const hasLogin = localStorage.getItem('token'); + const checkVersion = async (): Promise<{ id: string; version: number } | null> => { + const res = await useUserWallStore.getState().queryWallVersion(id); + if (res.code === 200) { + const data = res.data; + return data; + } else { + message.error('获取失败,请稍后刷新重试'); + return null; } - }, - }); - return; - } else set({ open, selectedNode: data }); - }, - selectedNode: null, - setSelectedNode: (node) => set({ selectedNode: node }), - editValue: '', - 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, - setId: (id) => set({ id }), - loaded: false, - init: async (id?: string | null) => { - // 如果登陆了且如果有id,从服务器获取 - // 没有id,获取缓存 - const hasLogin = localStorage.getItem('token'); - if (hasLogin && id) { - const res = await useUserWallStore.getState().queryWall(id); - if (res.code === 200) { - set({ nodes: res.data?.data?.nodes || [], loaded: true, id, data: res.data }); - } else { - // message.error('获取失败,请稍后刷新重试'); - set({ loaded: 'error' }); - } - } else if (!hasLogin && id) { - // 没有登陆,但是有id,从服务器获取 - // 跳转到登陆页面 - redirectToLogin(); - } else { - const data = await getWallData(); - 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, - setToolbarOpen: (open) => set({ toolbarOpen: open }), - showFormDialog: false, - setShowFormDialog: (show) => set({ showFormDialog: show }), - formDialogData: null, - setFormDialogData: (data) => set({ formDialogData: data }), - clear: async () => { - if (get().id) { - set({ nodes: [], selectedNode: null, editValue: '', data: null }); - await useUserWallStore.getState().saveWall({ - id: get().id!, - data: { - nodes: [], - }, - }); - } else { - 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 }); - // 导出为json - const json = JSON.stringify(covertData); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'wall.json'; - a.click(); - }, - clearQueryWall: async () => { - set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false }); - }, - mouseSelect: true, - setMouseSelect: (mouseSelect) => set({ mouseSelect }), - getNodeById: async (id: string) => { - const data = await getWallData(); - const nodes = data?.nodes || []; - return nodes.find((node) => node.id === id); - }, - saveNodeById: async (id: string, data: any) => { - let node = await get().getNodeById(id); - if (node) { - node.data = { - ...node.data, - ...data, - }; - const newNodes = get().nodes.map((item) => { - if (item.id === id) { - return node; + }; + const getNew = async () => { + const res = await useUserWallStore.getState().queryWall(id); + if (res.code === 200) { + const data = res.data; + set({ nodes: data?.data?.nodes || [], loaded: true, id: data?.id, data }); + setCacheWallData(data, data?.id); + } + }; + if (hasLogin) { + const cvData = await checkVersion(); + if (cvData) { + const id = cvData?.id; + const cacheData = await getCacheWallData(id); + if (cacheData) { + const version = cacheData?.version; + if (version === cvData?.version) { + set({ nodes: cacheData?.data?.nodes || [], loaded: true, id, data: cacheData }); + } else { + getNew(); + } + } else { + getNew(); + } + } + } else { + // 跳转到登陆页面 + redirectToLogin(); } - return item; - }); - set({ - nodes: newNodes, - }); - get().saveNodes(newNodes, { showMessage: false }); + }, + toolbarOpen: false, + setToolbarOpen: (open) => set({ toolbarOpen: open }), + showFormDialog: false, + setShowFormDialog: (show) => set({ showFormDialog: show }), + formDialogData: null, + setFormDialogData: (data) => set({ formDialogData: data }), + clear: async () => { + // if (get().id) { + // set({ nodes: [], data: null }); + // await useUserWallStore.getState().saveWall({ + // id: get().id!, + // data: { + // nodes: [], + // }, + // }); + // } else { + // set({ nodes: [], id: null, data: null }); + // await setCacheWallData({ nodes: [] }); + // } + }, + clearId: async () => { + set({ id: null, data: null }); + }, + + exportWall: async (nodes: NodeData[]) => { + const covertData = getNodeData(nodes); + const mark = get().data; + setCacheWallData({ ...mark, data: { ...mark.data, nodes: covertData } }, mark?.id); + // 导出为json + const json = JSON.stringify(covertData); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'wall.json'; + a.click(); + }, + clearQueryWall: async () => { + set({ nodes: [], id: null, data: null, toolbarOpen: false, loaded: false }); + }, + mouseSelect: true, + setMouseSelect: (mouseSelect) => set({ mouseSelect }), + getNodeById: async (id: string) => { + const data = await getCacheWallData(get().id!); + const nodes = data?.data?.nodes || []; + return nodes.find((node) => node.id === id); + }, + saveNodeById: async (id: string, data: any) => { + let node = await get().getNodeById(id); + if (node) { + node.data = { + ...node.data, + ...data, + updatedAt: new Date().getTime(), + }; + const newNodes = get().nodes.map((item) => { + if (item.id === id) { + return node; + } + return item; + }); + set({ + nodes: newNodes, + }); + get().saveNodes(newNodes, { showMessage: false }); + } + }, + })); + this.storeMap.set(id, store); + return store; + } + getStoreById(id: string) { + const store = this.storeMap.get(id); + if (!store) { + return this.crateStoreById(id); } - }, -})); + return store; + } +} +// export const useWallStore = +const wallStore = useContextKey('wallStore', () => new WallStore()); +export const useWallStore = wallStore.getStoreById('today'); diff --git a/src/pages/wall/utils/db.ts b/src/pages/wall/utils/db.ts index 6c5945e..c11d718 100644 --- a/src/pages/wall/utils/db.ts +++ b/src/pages/wall/utils/db.ts @@ -2,19 +2,19 @@ import { MyCache } from '@kevisual/cache'; const cache = new MyCache('cacheWall'); -export async function getWallData() { +export async function getCacheWallData(key?: string) { try { - const data = await cache.getData(); + const data = await cache.get(key ?? 'cacheWall'); return data; } catch (e) { cache.del(); } } -export async function setWallData(data: any) { - await cache.setData(data); +export async function setCacheWallData(data: any, key?: string) { + await cache.set(key ?? 'cacheWall', data); } -export async function clearWallData() { +export async function clearCacheWallData() { await cache.del(); } diff --git a/src/routes.tsx b/src/routes.tsx index c238615..ef322b3 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -4,6 +4,7 @@ import { useContextKey } from '@kevisual/system-lib/dist/web-config'; import './index.css'; import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser'; import { NodeTextEditor } from './pages/editor/NodeTextEditor.tsx'; +import { AiEditor } from './pages/editor/index.tsx'; import { Panels } from './modules/panels/index.tsx'; import { Page } from '@kevisual/system-lib/dist/web-page'; @@ -39,7 +40,7 @@ app app .route({ path: 'editor', - key: 'render', + key: 'nodeRender', description: '获取编辑器', }) .define(async (ctx) => { @@ -47,6 +48,16 @@ app }) .addTo(app); +app + .route({ + path: 'editor', + key: 'render', + description: '获取编辑器', + }) + .define(async (ctx) => { + ctx.body = { lib: AiEditor, type: 'react', Panels }; + }) + .addTo(app); app .route({ path: 'editor', diff --git a/template/command/routes.ts b/template/command/routes.ts new file mode 100644 index 0000000..47bc1bd --- /dev/null +++ b/template/command/routes.ts @@ -0,0 +1,121 @@ +import TurndownService from 'turndown'; +import { app, message } from '../app'; + +// 命令规则 +// 1. 命令以 ! 开头 +// 2. 命令和内容之间用空格隔开 +// 3. 多余的地方不要有!,如果有,使用\! 代替 +// +// +// test命令 !a 显示内容 !b 但是会计法 !c 飒短发 !fdsaf s !kong !d d!!的身份 ! 是的! !ene +// 7个 +export function parseCommands(text: string) { + //文本以\!的内容都去掉 + text = text.replace(/\\!/g, '__REPLACE__RETURN__'); + const result: { command: string; content: string }[] = []; + const parts = text.split('!'); + + for (let i = 1; i < parts.length; i++) { + const part = parts[i].trim(); + if (part.length === 0) continue; // 忽略空的部分 + + const spaceIndex = part.indexOf(' '); + const command = '!' + (spaceIndex === -1 ? part : part.slice(0, spaceIndex)); + let content = spaceIndex === -1 ? '' : part.slice(spaceIndex + 1).trim(); + if (content.includes('__REPLACE__RETURN__')) { + content = content.replace('__REPLACE__RETURN__', '!'); + } + result.push({ command, content }); + } + + return result; +} + +app + .route({ + path: 'command', + key: 'handle', + description: '处理命令', + }) + .define(async (ctx) => { + const { html } = ctx.query; + // 解析 文本中的 !command 命令 + // 1. 当没有命令的时候是保存文本 + // 2. 当有命令的时候,查询命令,执行 + // - 当命令不存在,直接返回提示 + // - 当命令存在,执行命令 + const turndown = new TurndownService(); + const markdown = turndown.turndown(html); + const commands = parseCommands(markdown); + + if (commands.length === 0) { + ctx.body = markdown; + const res = await app.call({ path: 'note', key: 'save', payload: { html } }); + if (res.code !== 200) { + message.error(res.message || '保存失败'); + ctx.throw(400, res.message || '保存失败'); + } + return; + } + console.log('md', markdown); + console.log('commands', commands, commands.length); + const res = await app.call({ path: 'command', key: 'list', payload: { commands } }); + }) + .addTo(app); + +app + .route({ + path: 'command', + key: 'list', + description: '命令列表', + metadata: { + command: 'command-list', + prompt: '把当前我的数据中,所有命令列表返回', + }, + validator: { + commands: { + type: 'any', + required: false, + }, + }, + }) + .define(async (ctx) => { + const { commands } = ctx.query; + const getRouteInfo = (route: any) => { + return { + path: route.path, + key: route.key, + description: route.description, + metadata: route.metadata, + validator: route.validator, + }; + }; + if (Array.isArray(commands) && commands.length > 0) { + const routes = ctx.queryRouter.routes; + const commandRoutes = commands.map((command) => { + const route = routes.find((route) => route.metadata?.command === command.command); + if (!route) { + message.error(`命令 ${command.command} 不存在`); + ctx.throw(400, `命令 ${command.command} 不存在`); + } + return { + command, + route: getRouteInfo(route), + }; + }); + ctx.body = commandRoutes; + } else { + ctx.body = ctx.queryRouter.routes + .map((route) => ({ + command: route.metadata?.command, + route: getRouteInfo(route), + })) + .filter((item) => item.command); + } + }) + .addTo(app); + +setTimeout(async () => { + const res = await app.call({ path: 'command', key: 'list' }); + console.log('list', res.body); +}, 2000); diff --git a/template/index.ts b/template/index.ts index 57989bd..fb93199 100644 --- a/template/index.ts +++ b/template/index.ts @@ -3,6 +3,7 @@ import '../src/routes'; import './ai-app/main'; import './tailwind.css'; import './workspace/entry'; +import './routes'; page.addPage('/', 'workspace'); diff --git a/template/routes.ts b/template/routes.ts new file mode 100644 index 0000000..8770bb0 --- /dev/null +++ b/template/routes.ts @@ -0,0 +1,2 @@ +import './user/route'; +import './command/routes'; \ No newline at end of file diff --git a/template/user/route.ts b/template/user/route.ts index 7b1738d..9897bdb 100644 --- a/template/user/route.ts +++ b/template/user/route.ts @@ -1,6 +1,45 @@ -import { app } from '../app'; +import { app, message } from '../app'; -app.route({ - path: 'user', - key: 'login', -}); +app + .route({ + path: 'user', + key: 'login', + }) + .define(async (ctx) => { + const { username, password } = ctx.query; + if (!username || !password) { + message.error('用户名和密码不能为空'); + ctx.throw(400, '用户名和密码不能为空'); + } + const res = await fetch('/api/router', { + method: 'POST', + body: JSON.stringify({ path: 'user', key: 'login', username, password }), + }).then((res) => res.json()); + if (res.code === 200) { + localStorage.setItem('token', res.data.token); + } else { + message.error(res.message); + ctx.throw(400, res.message); + } + }) + .addTo(app); + +app + .route({ + path: 'user', + key: 'logout', + description: '退出登录', + metadata: { + command: 'logout', + }, + }) + .define(async (ctx) => { + localStorage.removeItem('token'); + fetch('/api/router?path=user&key=logout', { + method: 'POST', + }); + setTimeout(() => { + window.location.href = '/user/login'; + }, 1000); + }) + .addTo(app); diff --git a/template/workspace/prompts/html示例.md b/template/workspace/prompts/html示例.md new file mode 100644 index 0000000..f55eeee --- /dev/null +++ b/template/workspace/prompts/html示例.md @@ -0,0 +1,36 @@ +把当前我的数据中,所有的title和description和path和key列出来,生成一个好看的卡片式的列表。只给我返回html的内容,其他的东西不返回给我。 + +```json +[ + { + "command": "logout", + "route": { + "path": "user", + "key": "logout", + "description": "退出登录", + "metadata": { + "command": "logout" + }, + "validator": {} + } + }, + { + "command": "command-list", + "route": { + "path": "command", + "key": "list", + "description": "命令列表", + "metadata": { + "command": "command-list", + "prompt": "把当前我的数据中,所有命令列表返回" + }, + "validator": { + "commands": { + "type": "any", + "required": false + } + } + } + } +] +``` diff --git a/template/workspace/prompts/命令列表.md b/template/workspace/prompts/命令列表.md new file mode 100644 index 0000000..bd1b844 --- /dev/null +++ b/template/workspace/prompts/命令列表.md @@ -0,0 +1,6 @@ +我有一个命令列表,我需要通过查询去获取相应的列表的内容,我提供你查询的方式。我需要你把我文本的内容转为查询的参数的格式。 + + + + + diff --git a/template/workspace/prompts/提取指令.md b/template/workspace/prompts/提取指令.md new file mode 100644 index 0000000..d83c9d4 --- /dev/null +++ b/template/workspace/prompts/提取指令.md @@ -0,0 +1,7 @@ +我有一些命令匹配的文本,格式是: !command text-content 他是很多类同的命令结合一起的,其中text-content可能为空,其中命令和内容都可能是乱拼的,只要符合 !command ,你就要把内容返回给我。其中如果!单独存在,或者!之前面有内容,都不属于命令,都属于上一个命令的文本,你需要排出这些错误情况。你需要把命令和文本的内容返回给我一个json数据。返回的格式是[{command,content],你只需要把你对应的内容返回给我,不要返回其他内容。 + +我给你的命令文本是 + +!a 显示内容 !b 但是会计法 !c 飒短发 !fdsaf s !d d!!的身份 ! 是的! !ene + +PROMPT_TEXT \ No newline at end of file diff --git a/template/workspace/提取指令.md b/template/workspace/提取指令.md new file mode 100644 index 0000000..e69de29