This commit is contained in:
2025-03-12 00:50:44 +08:00
parent 4a04a432ca
commit cbef0943de
30 changed files with 815 additions and 549 deletions

View 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;
}

View 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>
);
};

View File

@@ -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',

View File

@@ -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>
);
};

View File

@@ -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 });
// }
// },
// });

View File

@@ -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>`;

View File

@@ -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]);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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'>

View File

@@ -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');

View File

@@ -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');

View File

@@ -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();
}