generated from template/vite-react-template
temp
This commit is contained in:
parent
4a04a432ca
commit
cbef0943de
@ -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;
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className=' pointer-events-auto fixed w-full overflow-x-auto bottom-0 left-0 right-0 bg-gray-200 text-white p-2 flex space-x-2 z-[9000] h-[40px]'>
|
||||
{showLogout && (
|
||||
<div
|
||||
className='flex items-center space-x-2 cursor-pointer bg-blue-600 rounded-md p-1'
|
||||
onClick={() => {
|
||||
context?.app?.call?.({
|
||||
path: 'user',
|
||||
key: 'logout',
|
||||
});
|
||||
}}>
|
||||
<LogOut size={16} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='flex items-center space-x-2 cursor-pointer bg-blue-600 rounded-md p-1'
|
||||
onClick={() => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -90,7 +90,8 @@ export const usePanelStore = create<PanelStore>((set, get) => ({
|
||||
|
||||
set({
|
||||
data: {
|
||||
windows: [e.windowData],
|
||||
// windows: [e.windowData],
|
||||
windows: [],
|
||||
showTaskbar: true,
|
||||
},
|
||||
});
|
||||
@ -136,8 +137,9 @@ export const usePanelStore = create<PanelStore>((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<PanelStore>((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<PanelStore>((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);
|
||||
|
40
src/pages/demo-login/index.css
Normal file
40
src/pages/demo-login/index.css
Normal file
@ -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;
|
||||
}
|
27
src/pages/demo-login/index.tsx
Normal file
27
src/pages/demo-login/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Button } from '@mui/material';
|
||||
import './index.css';
|
||||
export const AppendDemo = () => {
|
||||
return (
|
||||
<div className='demo-login-prompt'>
|
||||
<p className='demo-login-link'>
|
||||
<a href='/user/login'>请先登录</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DemoLogin = () => {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center h-screen gap-4'>
|
||||
<AppendDemo />
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
window.location.href = `/user/login/?redirect=${window.location.href}&username=demo&password=xiong1015`;
|
||||
}}>
|
||||
试用登录
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<TextEditor | null>(null);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
@ -61,7 +66,7 @@ export const NodeTextEditor = ({ id }: EditorProps) => {
|
||||
};
|
||||
const exitEdit = () => {
|
||||
// 退出编辑
|
||||
saveContent()
|
||||
saveContent();
|
||||
setTimeout(() => {
|
||||
app.call({
|
||||
path: 'panels',
|
||||
|
@ -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<TextEditor | null>(null);
|
||||
const editorRef = useRef<HTMLDivElement>(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 (
|
||||
<div className={clsx('w-full h-full editor-container relative', className)}>
|
||||
<div ref={editorRef} className={clsx('w-full h-full node-editor', className)}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -22,3 +22,14 @@ export function SplitButtons({ closeToast }: ToastContentProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 });
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
@ -1 +1,10 @@
|
||||
export const BlankNoteText = '<i>double click to edit</i>';
|
||||
|
||||
// https://www.reactbits.dev/text-animations/circular-text
|
||||
export const CircularText =
|
||||
'<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=114057235467353&bvid=BV1ALPseyE7n&cid=28549582521&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
|
||||
|
||||
export const CircularText2 =
|
||||
'<iframe src="https://kevisual.xiongxiao.me/apps/questions/" sandbox="allow-scripts allow-same-origin" width="100%" height="100%" ></iframe>';
|
||||
|
||||
export const CircularText3 = `<iframe srcdoc="<p>Your content here</p>"></iframe>`;
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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() {
|
||||
<Toolbar />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Drawer />
|
||||
<SaveModal />
|
||||
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
||||
</Panel>
|
||||
</ReactFlow>{' '}
|
||||
</ReactFlow>
|
||||
{isSelecting && selectionBox && (
|
||||
<div
|
||||
style={{
|
||||
@ -209,10 +216,11 @@ export function FlowContent() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 <DemoLogin />;
|
||||
}
|
||||
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 <div>loading...</div>;
|
||||
} else if (wallStore.loaded === 'error') {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center h-screen gap-4'>
|
||||
<div className='text-2xl font-bold'>获取失败,请稍后刷新重试,或者转到首页</div>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
// navigate('/');
|
||||
wallStore.clearId();
|
||||
}}>
|
||||
转到首页
|
||||
</Button>
|
||||
<div className='text-2xl font-bold'>获取失败,请稍后刷新重试</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -251,13 +250,3 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
export const FlowStatus = () => {
|
||||
const { nodes } = useWallStore();
|
||||
const reactFlow = useReactFlow();
|
||||
const flowStore = useStore((state) => state);
|
||||
return (
|
||||
<div>
|
||||
<div>节点数量: {nodes.length}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -12,13 +12,10 @@ export type WallData<T = Record<string, any>> = {
|
||||
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<HTMLDivElement>(null);
|
||||
if (!html) return <div className='w-full h-full flex items-center justify-center '>空</div>;
|
||||
const ShowContent = (props: { data: WallData; id: string; selected: boolean }) => {
|
||||
const [highlightHtml, setHighlightHtml] = useState('');
|
||||
const highlight = async (html: string) => {
|
||||
const _html = html.replace(/<pre><code class="language-(\w+)">([\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 (
|
||||
<div
|
||||
ref={showRef}
|
||||
className='p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white markdown-body'
|
||||
className={clsx('p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white markdown-body', {}, 'id' + props.id)}
|
||||
style={{
|
||||
pointerEvents: selected ? 'auto' : 'none',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: highlightHtml }}></div>
|
||||
pointerEvents: props.selected ? 'auto' : 'none',
|
||||
}}></div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<div
|
||||
className={clsx('absolute -top-2 left-0 bg-gray-300 z-10 w-full h-2 custom-dragger cursor-move', {
|
||||
'opacity-0': !props.selected,
|
||||
})}
|
||||
style={{
|
||||
width: `calc(100% + ${handleSize}px)`,
|
||||
transform: `translateX(-${handleSize / 2}px)`,
|
||||
}}></div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
onDoubleClick={(e) => {
|
||||
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}>
|
||||
<ShowContent data={data} selected={props.selected} />
|
||||
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,
|
||||
}}>
|
||||
<ShowContent data={data} id={props.id} selected={props.selected} />
|
||||
</div>
|
||||
<div className={clsx('absolute top-0 right-0 cursor-pointer', props.selected ? 'opacity-100' : 'opacity-0')}>
|
||||
<button
|
||||
@ -146,6 +144,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
minHeight={50}
|
||||
onResizeStart={() => {}}
|
||||
isVisible={props.selected}
|
||||
color='#d1d5dc'
|
||||
onResizeEnd={(e) => {
|
||||
const parent = contentRef.current?.parentElement;
|
||||
if (!parent) return;
|
||||
@ -161,6 +160,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
? {
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
border: 'unset',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
import { useWallStore } from '../store/wall'; // 确保导入正确的路径
|
||||
import clsx from 'clsx';
|
||||
import { X } from 'lucide-react'; // 导入 Close 图标
|
||||
import { Editor } from '@/pages/editor';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStore, useStoreApi } from '@xyflow/react';
|
||||
import { BlankNoteText } from '../constants';
|
||||
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, hasEdited, setHasEdited } = useWallStore(
|
||||
useShallow((state) => ({
|
||||
open: state.open,
|
||||
setOpen: state.setOpen,
|
||||
selectedNode: state.selectedNode,
|
||||
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, true);
|
||||
}
|
||||
}, [open, selectedNode]);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => {
|
||||
setOpen(false);
|
||||
setHasEdited(false);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
}, []);
|
||||
const listener = async (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const systemKey = e.metaKey || e.ctrlKey;
|
||||
|
||||
// mac command+s windows ctrl+s
|
||||
if (systemKey && e.key === 's') {
|
||||
onSave();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', listener);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', listener);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!open && mounted) {
|
||||
if (hasEdited) {
|
||||
onSave();
|
||||
}
|
||||
}
|
||||
}, [open, hasEdited, mounted]);
|
||||
const onSave = () => {
|
||||
const wallStore = useWallStore.getState();
|
||||
const selectedNode = wallStore.selectedNode;
|
||||
const _editValue = wallStore.editValue;
|
||||
if (selectedNode && _editValue) {
|
||||
selectedNode.data.html = _editValue;
|
||||
const newNodes = storeApi.getState().nodes.map((node) => (node.id === selectedNode.id ? selectedNode : node));
|
||||
storeApi.setState({ nodes: newNodes });
|
||||
if (wallStore.id) {
|
||||
message.success('保存到服务器成功', {
|
||||
closeOnClick: true,
|
||||
});
|
||||
} else {
|
||||
message.success('保存到本地成功', {
|
||||
closeOnClick: true,
|
||||
});
|
||||
}
|
||||
wallStore.saveNodes(newNodes, { showMessage: false });
|
||||
}
|
||||
};
|
||||
let html = selectedNode?.data?.html || '';
|
||||
if (html === BlankNoteText) {
|
||||
html = '';
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'transition-all duration-300 bg-white flex flex-col gap-2 h-full w-full overflow-hidden fixed right-0 top-0 z-10',
|
||||
open ? 'open' : 'hidden',
|
||||
'w-[800px] xs:w-[100%] sm:w-[100%] md:w-[600px] lg:w-[600px] xl:w-[600px] 2xl:w-[800px]', // 默认宽度,根据屏幕大小适配,小屏幕全屏幕
|
||||
)}>
|
||||
<div className='flex justify-between items-center h-10'>
|
||||
<button onClick={() => setOpen(false)}>
|
||||
<X className='w-6 h-6 cursor-pointer ml-2' />
|
||||
</button>
|
||||
{selectedNode && (
|
||||
<div>
|
||||
<button className='bg-blue-500 text-white px-4 py-1 rounded-md mr-4 cursor-pointer' onClick={onSave}>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className='pr-4 mx-4 mb-4 rounded-md pb-4 box-border scrollbar border border-gray-300 '
|
||||
style={{
|
||||
height: 'calc(100vh - 2.5rem)',
|
||||
overflowY: 'auto',
|
||||
}}>
|
||||
{selectedNode && open && <Editor className='drawer-editor' value={html} onChange={setEditValue} id={selectedNode.id} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Drawer;
|
@ -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;
|
||||
}
|
||||
|
@ -130,115 +130,42 @@ export const ToolbarContent = ({ open }) => {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '清空',
|
||||
key: 'clear',
|
||||
icon: <Trash />,
|
||||
onClick: async () => {
|
||||
await wallStore.clear();
|
||||
message.success('清空成功');
|
||||
store.setNodes([]);
|
||||
},
|
||||
},
|
||||
];
|
||||
if (hasLogin) {
|
||||
menuList.unshift({
|
||||
label: '我的笔记',
|
||||
key: 'myWall',
|
||||
icon: <BrickWall />,
|
||||
onClick: () => {
|
||||
//
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!hasLogin) {
|
||||
menuList.push({
|
||||
label: '登录',
|
||||
key: 'login',
|
||||
icon: <User />,
|
||||
onClick: () => {
|
||||
redirectToLogin();
|
||||
},
|
||||
});
|
||||
if (wallStore.id) {
|
||||
menuList.push({
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: <Trash />,
|
||||
onClick: async () => {
|
||||
const res = await userWallStore.deleteWall(wallStore.id!);
|
||||
if (res.code === 200) {
|
||||
// navigate('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!wallStore.id) {
|
||||
menuList.push({
|
||||
label: '保存到账号',
|
||||
key: 'saveToAccount',
|
||||
icon: <Save />,
|
||||
onClick: () => {
|
||||
wallStore.setShowFormDialog(true);
|
||||
wallStore.setFormDialogData({
|
||||
title: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
summary: '',
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuList.push({
|
||||
label: '编辑信息',
|
||||
key: 'saveToAccount',
|
||||
icon: <Save />,
|
||||
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: <Plus />,
|
||||
onClick: () => {
|
||||
// navigate(`/`);
|
||||
wallStore.clearQueryWall();
|
||||
},
|
||||
});
|
||||
menuList.push({
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: <Trash />,
|
||||
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: <Trash />,
|
||||
onClick: async () => {
|
||||
const res = await userWallStore.deleteWall(wallStore.id!);
|
||||
if (res.code === 200) {
|
||||
// navigate('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
menuList.push({
|
||||
label: '退出 ',
|
||||
key: 'logout',
|
||||
icon: <User />,
|
||||
onClick: () => {
|
||||
userWallStore.logout();
|
||||
},
|
||||
});
|
||||
}
|
||||
menuList.push({
|
||||
label: '编辑信息',
|
||||
key: 'saveToAccount',
|
||||
icon: <Save />,
|
||||
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: <User />,
|
||||
onClick: () => {
|
||||
userWallStore.logout();
|
||||
},
|
||||
});
|
||||
return (
|
||||
<ClickAwayListener onClickAway={() => wallStore.setToolbarOpen(false)}>
|
||||
<div className=' flex flex-col items-center w-[200px] bg-white border border-gray-300 rounded-md absolute top-0 left-8'>
|
||||
|
@ -30,8 +30,11 @@ interface UserWallStore {
|
||||
wallList: Wall[];
|
||||
queryWallList: () => Promise<void>;
|
||||
logout: () => void;
|
||||
saveWall: (data: Wall, opts?: { refresh?: boolean, showMessage?: boolean }) => Promise<any>;
|
||||
queryWall: (id: string) => Promise<any>;
|
||||
saveWall: (data: Wall, opts?: { refresh?: boolean; showMessage?: boolean }) => Promise<any>;
|
||||
saveOneNode: (id: string, node: any) => Promise<any>;
|
||||
saveDataNodes: (id: string, nodes: any[], opts?: { showMessage?: boolean }) => Promise<any>;
|
||||
queryWall: (id?: string) => Promise<any>;
|
||||
queryWallVersion: (id?: string) => Promise<any>;
|
||||
deleteWall: (id: string) => Promise<any>;
|
||||
}
|
||||
|
||||
@ -66,7 +69,7 @@ export const useUserWallStore = create<UserWallStore>((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<UserWallStore>((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<UserWallStore>((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');
|
||||
|
@ -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<T = { [key: string]: any }> = {
|
||||
id: string;
|
||||
position: XYPosition;
|
||||
@ -27,23 +26,14 @@ interface WallState {
|
||||
// 只做传递
|
||||
nodes: NodeData[];
|
||||
setNodes: (nodes: NodeData[]) => void;
|
||||
saveDataNode: (nodes: NodeData[]) => Promise<void>;
|
||||
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, init?: boolean) => void;
|
||||
hasEdited: boolean;
|
||||
setHasEdited: (hasEdited: boolean) => void;
|
||||
data?: any;
|
||||
setData: (data: any) => void;
|
||||
init: (id?: string | null) => Promise<void>;
|
||||
init: (id?: string) => Promise<void>;
|
||||
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<NodeData | null>;
|
||||
saveNodeById: (id: string, data: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useWallStore = create<WallState>((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<string, UseBoundStore<StoreApi<WallState>>> = new Map();
|
||||
constructor() {
|
||||
this.crateStoreById('today');
|
||||
}
|
||||
crateStoreById(id: string) {
|
||||
const store = create<WallState>((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<WallState>((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');
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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',
|
||||
|
121
template/command/routes.ts
Normal file
121
template/command/routes.ts
Normal file
@ -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);
|
@ -3,6 +3,7 @@ import '../src/routes';
|
||||
import './ai-app/main';
|
||||
import './tailwind.css';
|
||||
import './workspace/entry';
|
||||
import './routes';
|
||||
|
||||
page.addPage('/', 'workspace');
|
||||
|
||||
|
2
template/routes.ts
Normal file
2
template/routes.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import './user/route';
|
||||
import './command/routes';
|
@ -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);
|
||||
|
36
template/workspace/prompts/html示例.md
Normal file
36
template/workspace/prompts/html示例.md
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
6
template/workspace/prompts/命令列表.md
Normal file
6
template/workspace/prompts/命令列表.md
Normal file
@ -0,0 +1,6 @@
|
||||
我有一个命令列表,我需要通过查询去获取相应的列表的内容,我提供你查询的方式。我需要你把我文本的内容转为查询的参数的格式。
|
||||
|
||||
|
||||
|
||||
|
||||
|
7
template/workspace/prompts/提取指令.md
Normal file
7
template/workspace/prompts/提取指令.md
Normal file
@ -0,0 +1,7 @@
|
||||
我有一些命令匹配的文本,格式是: !command text-content 他是很多类同的命令结合一起的,其中text-content可能为空,其中命令和内容都可能是乱拼的,只要符合 !command ,你就要把内容返回给我。其中如果!单独存在,或者!之前面有内容,都不属于命令,都属于上一个命令的文本,你需要排出这些错误情况。你需要把命令和文本的内容返回给我一个json数据。返回的格式是[{command,content],你只需要把你对应的内容返回给我,不要返回其他内容。
|
||||
|
||||
我给你的命令文本是
|
||||
|
||||
!a 显示内容 !b 但是会计法 !c 飒短发 !fdsaf s !d d!!的身份 ! 是的! !ene
|
||||
|
||||
PROMPT_TEXT
|
0
template/workspace/提取指令.md
Normal file
0
template/workspace/提取指令.md
Normal file
Loading…
x
Reference in New Issue
Block a user