generated from template/vite-react-template
add wallnote
This commit is contained in:
149
src/pages/wall/modules/CustomNode.css
Normal file
149
src/pages/wall/modules/CustomNode.css
Normal file
@@ -0,0 +1,149 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@layer components {
|
||||
.node-editor {
|
||||
@apply w-full h-full bg-white;
|
||||
> div {
|
||||
@apply w-full h-full outline-none;
|
||||
}
|
||||
}
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
display: block;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar-thumb:horizontal {
|
||||
background-color: #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--purple-light: #e0e0ff; /* 默认浅紫色背景 */
|
||||
--black: #000000; /* 默认黑色 */
|
||||
--white: #ffffff; /* 默认白色 */
|
||||
--gray-3: #d3d3d3; /* 默认灰色3 */
|
||||
--gray-2: #e5e5e5; /* 默认灰色2 */
|
||||
}
|
||||
.tiptap-preview {
|
||||
.tiptap {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
.tiptap {
|
||||
margin: 0.5rem 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
/* Basic editor styles */
|
||||
.tiptap:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
.tiptap ul,
|
||||
.tiptap ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
}
|
||||
|
||||
.tiptap ul li p,
|
||||
.tiptap ol li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
.tiptap h1,
|
||||
.tiptap h2,
|
||||
.tiptap h3,
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.tiptap h1,
|
||||
.tiptap h2 {
|
||||
/* margin-top: 3.5rem; */
|
||||
margin-top: 1rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.tiptap h1 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tiptap h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiptap h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
.tiptap code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
.tiptap pre {
|
||||
border: 1px solid #ccc;
|
||||
/* background: var(--black); */
|
||||
border-radius: 0.5rem;
|
||||
/* color: var(--white); */
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.tiptap pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tiptap mark {
|
||||
background-color: #FAF594;
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
.tiptap blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.tiptap hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
150
src/pages/wall/modules/CustomNode.tsx
Normal file
150
src/pages/wall/modules/CustomNode.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useRef, memo, useEffect, useMemo, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { NodeResizer, useStore } from '@xyflow/react';
|
||||
import { useWallStore } from '../store/wall';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { toast } from 'react-toastify';
|
||||
import { message } from '@/modules/message';
|
||||
import hljs from 'highlight.js';
|
||||
import { Edit } from 'lucide-react';
|
||||
export type WallData<T = Record<string, any>> = {
|
||||
html: string;
|
||||
width?: number;
|
||||
height?: 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 [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) => {
|
||||
return `<pre><code class="language-${p1}">${hljs.highlight(p2, { language: p1 }).value}</code></pre>`;
|
||||
});
|
||||
return _html;
|
||||
};
|
||||
useEffect(() => {
|
||||
highlight(html).then((res) => {
|
||||
setHighlightHtml(res);
|
||||
});
|
||||
}, [html]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={showRef}
|
||||
className='p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white'
|
||||
style={{
|
||||
pointerEvents: selected ? 'auto' : 'none',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: highlightHtml }}></div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomNode = (props: { id: string; data: WallData; selected: boolean }) => {
|
||||
const data = props.data;
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const selected = props.selected;
|
||||
const wallStore = useWallStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
setOpen: state.setOpen,
|
||||
setSelectedNode: state.setSelectedNode,
|
||||
saveNodes: state.saveNodes,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const store = useStore((state) => {
|
||||
return {
|
||||
updateWallRect: (id: string, rect: { width: number; height: number }) => {
|
||||
const nodes = state.nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
node.data.width = rect.width;
|
||||
node.data.height = rect.height;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
state.setNodes(nodes);
|
||||
wallStore.saveNodes(nodes);
|
||||
},
|
||||
getNode: (id: string) => {
|
||||
return state.nodes.find((node) => node.id === id);
|
||||
},
|
||||
deleteNode: (id: string) => {
|
||||
const nodes = state.nodes.filter((node) => node.id !== id);
|
||||
state.setNodes(nodes);
|
||||
wallStore.saveNodes(nodes);
|
||||
},
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
const handleDelete = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Delete') {
|
||||
store.deleteNode(props.id);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleDelete);
|
||||
return () => window.removeEventListener('keydown', handleDelete);
|
||||
}
|
||||
}, [selected]);
|
||||
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);
|
||||
if (node) {
|
||||
wallStore.setOpen(true);
|
||||
wallStore.setSelectedNode(node);
|
||||
} else {
|
||||
message.error('节点不存在');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<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} />
|
||||
</div>
|
||||
<div className={clsx('absolute top-0 right-0', props.selected ? 'opacity-100' : 'opacity-0')}>
|
||||
<button
|
||||
className='w-6 h-6 flex items-center justify-center'
|
||||
onClick={() => {
|
||||
showOpen();
|
||||
}}>
|
||||
<Edit className='w-4 h-4' />
|
||||
</button>
|
||||
</div>
|
||||
<NodeResizer
|
||||
minWidth={100}
|
||||
minHeight={50}
|
||||
onResizeStart={() => {}}
|
||||
isVisible={props.selected}
|
||||
onResizeEnd={(e) => {
|
||||
const parent = contentRef.current?.parentElement;
|
||||
if (!parent) return;
|
||||
const width = parent.style.width;
|
||||
const height = parent.style.height;
|
||||
const widthNum = parseInt(width);
|
||||
const heightNum = parseInt(height);
|
||||
if (!heightNum || !widthNum) return;
|
||||
store.updateWallRect(props.id, { width: widthNum, height: heightNum });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const WallNoteNode = memo(CustomNode);
|
||||
export const CustomNodeType = {
|
||||
wall: WallNoteNode,
|
||||
};
|
||||
106
src/pages/wall/modules/Drawer.tsx
Normal file
106
src/pages/wall/modules/Drawer.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
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 } = useWallStore(
|
||||
useShallow((state) => ({
|
||||
open: state.open,
|
||||
setOpen: state.setOpen,
|
||||
selectedNode: state.selectedNode,
|
||||
setSelectedNode: state.setSelectedNode,
|
||||
editValue: state.editValue,
|
||||
setEditValue: state.setEditValue,
|
||||
})),
|
||||
);
|
||||
const store = useStore((state) => state);
|
||||
const storeApi = useStoreApi();
|
||||
useEffect(() => {
|
||||
if (open && selectedNode) {
|
||||
setEditValue(selectedNode?.data.html);
|
||||
}
|
||||
}, [open, selectedNode]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setOpen(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);
|
||||
};
|
||||
}, []);
|
||||
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,
|
||||
});
|
||||
}
|
||||
wallStore.saveNodes(newNodes);
|
||||
}
|
||||
};
|
||||
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;
|
||||
132
src/pages/wall/modules/FormDialog.tsx
Normal file
132
src/pages/wall/modules/FormDialog.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useState } 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';
|
||||
import { useReactFlow, useStore } from '@xyflow/react';
|
||||
import { useUserWallStore } from '../store/user-wall';
|
||||
import { message } from '@/modules/message';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function FormDialog({ open, handleClose, handleSubmit, initialData }) {
|
||||
const [data, setData] = useState(initialData || { title: '', description: '', summary: '', tags: [] });
|
||||
|
||||
const handleChange = (event) => {
|
||||
setData({ ...data, [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
const handleTagDelete = (tagToDelete) => {
|
||||
setData({ ...data, tags: data.tags.filter((tag) => tag !== tagToDelete) });
|
||||
};
|
||||
|
||||
const handleAddTag = (event) => {
|
||||
if (event.key === 'Enter' && event.target.value !== '') {
|
||||
setData({ ...data, tags: [...data.tags, event.target.value] });
|
||||
event.target.value = ''; // Clear input after adding tag
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle>{initialData ? 'Edit Data' : 'Create Data'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin='dense'
|
||||
name='title'
|
||||
label='Title'
|
||||
type='text'
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
value={data.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
margin='dense'
|
||||
name='description'
|
||||
label='Description'
|
||||
type='text'
|
||||
fullWidth
|
||||
multiline
|
||||
variant='outlined'
|
||||
value={data.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
margin='dense'
|
||||
name='summary'
|
||||
label='Summary'
|
||||
type='text'
|
||||
fullWidth
|
||||
multiline
|
||||
variant='outlined'
|
||||
value={data.summary}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
margin='dense'
|
||||
name='tags'
|
||||
label='Tags'
|
||||
type='text'
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
placeholder='Press enter to add tags'
|
||||
onKeyPress={handleAddTag}
|
||||
/>
|
||||
{data.tags.map((tag, index) => (
|
||||
<Chip key={index} label={tag} onDelete={() => handleTagDelete(tag)} style={{ margin: '5px' }} />
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button onClick={() => handleSubmit(data)}>Submit</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormDialog;
|
||||
|
||||
export const SaveModal = () => {
|
||||
const wallStore = useWallStore(useShallow((state) => state));
|
||||
const userWallStore = useUserWallStore(useShallow((state) => state));
|
||||
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: 'wall' as 'wall',
|
||||
data,
|
||||
};
|
||||
const res = await userWallStore.saveWall(fromData, { refresh: true });
|
||||
if (res.code === 200) {
|
||||
setShowFormDialog(false);
|
||||
if (!id) {
|
||||
// 新创建
|
||||
const data = res.data;
|
||||
wallStore.clear();
|
||||
setTimeout(() => {
|
||||
navigate(`/wall/${data.id}`);
|
||||
}, 2000);
|
||||
} else {
|
||||
// 编辑
|
||||
wallStore.setData(res.data);
|
||||
}
|
||||
} else {
|
||||
message.error('保存失败');
|
||||
}
|
||||
};
|
||||
if (!showFormDialog) {
|
||||
return null;
|
||||
}
|
||||
return <FormDialog open={showFormDialog} handleClose={() => setShowFormDialog(false)} handleSubmit={onSubmit} initialData={formDialogData} />;
|
||||
};
|
||||
272
src/pages/wall/modules/toolbar/Toolbar.tsx
Normal file
272
src/pages/wall/modules/toolbar/Toolbar.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { PanelTopOpen, PanelTopClose, Save, Download, Upload, User, Trash, Plus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useWallStore } from '../../store/wall';
|
||||
import clsx from 'clsx';
|
||||
import { useUserWallStore } from '../../store/user-wall';
|
||||
import { redirectToLogin } from '@/modules/require-to-login';
|
||||
import { useStore } from '@xyflow/react';
|
||||
import { message } from '@/modules/message';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ClickAwayListener } from '@mui/material';
|
||||
export const ToolbarItem = ({
|
||||
children,
|
||||
showBorder = true,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
showBorder?: boolean;
|
||||
onClick?: () => any;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div onClick={onClick} className={clsx('flex items-center w-full gap-4 p-2 border-b border-gray-300 cursor-pointer', showBorder && 'border-b', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// 空白处点击,当不包函toolbar时候,关闭toolbar
|
||||
export const useBlankClick = () => {
|
||||
const { setToolbarOpen } = useWallStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
setToolbarOpen: state.setToolbarOpen,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// 点击的内容,closest('.toolbar')
|
||||
const target = e.target as HTMLElement;
|
||||
const toolbar = target.closest('.toolbar'); // 往上找,找到toolbar为止
|
||||
console.log('toolbar', target, toolbar);
|
||||
// if (!toolbar) {
|
||||
// setToolbarOpen(false);
|
||||
// }
|
||||
};
|
||||
console.log('add event');
|
||||
document.addEventListener('click', handleClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
export const ToolbarContent = ({ open }) => {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
const wallStore = useWallStore(useShallow((state) => state));
|
||||
const userWallStore = useUserWallStore(useShallow((state) => state));
|
||||
const store = useStore((state) => state);
|
||||
const hasLogin = !!userWallStore.user;
|
||||
const navigate = useNavigate();
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
key: string;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClick: () => any;
|
||||
};
|
||||
const menuList: MenuItem[] = [
|
||||
{
|
||||
label: '导出',
|
||||
key: 'export',
|
||||
icon: <Download />,
|
||||
onClick: () => {
|
||||
wallStore.exportWall(store.nodes);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '导入',
|
||||
key: 'import',
|
||||
icon: <Upload />,
|
||||
children: (
|
||||
<>
|
||||
<div>
|
||||
<Upload />
|
||||
</div>
|
||||
<div>导入</div>
|
||||
<input
|
||||
type='file'
|
||||
id='import-file'
|
||||
accept='.json'
|
||||
style={{ display: 'none' }}
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const data = e.target?.result;
|
||||
const json = JSON.parse(data as string);
|
||||
const keys = ['id', 'type', 'position', 'data'];
|
||||
if (Array.isArray(json) && json.every((item) => keys.every((key) => item[key]))) {
|
||||
const nodes = store.nodes;
|
||||
const newNodes = json.filter((item) => {
|
||||
return !nodes.find((node) => node.id === item.id);
|
||||
});
|
||||
const _nodes = [...nodes, ...newNodes];
|
||||
store.setNodes(_nodes);
|
||||
wallStore.saveNodes(_nodes);
|
||||
} else {
|
||||
message.error('文件格式错误');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
onClick: () => {
|
||||
const input = document.querySelector('#import-file')! as HTMLInputElement;
|
||||
if (input) {
|
||||
input.click();
|
||||
} else {
|
||||
message.error('请选择文件');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '清空',
|
||||
key: 'clear',
|
||||
icon: <Trash />,
|
||||
onClick: async () => {
|
||||
await wallStore.clear();
|
||||
message.success('清空成功');
|
||||
store.setNodes([]);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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: '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'>
|
||||
{menuList.map((item) => (
|
||||
<ToolbarItem
|
||||
key={item.key}
|
||||
className={item.className}
|
||||
onClick={() => {
|
||||
item.onClick?.();
|
||||
if (item.key !== 'import') {
|
||||
wallStore.setToolbarOpen(false);
|
||||
}
|
||||
}}>
|
||||
{item.children ? (
|
||||
<>{item.children}</>
|
||||
) : (
|
||||
<>
|
||||
<div>{item.icon}</div>
|
||||
<div>{item.label}</div>
|
||||
</>
|
||||
)}
|
||||
</ToolbarItem>
|
||||
))}
|
||||
<div className='text-xs p-1 text-gray-500 italic'>{wallStore.id ? 'id:' + wallStore.id : '临时编辑,资源缓存在本地'}</div>
|
||||
{hasLogin && <div className='text-xs p-1 -mt-1 text-gray-500 w-full text-right mr-2'>用户: {userWallStore.user?.username}</div>}
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
};
|
||||
export const Toolbar = () => {
|
||||
const wallStore = useWallStore(useShallow((state) => state));
|
||||
const { toolbarOpen, setToolbarOpen } = wallStore;
|
||||
|
||||
return (
|
||||
<div className='toolbar flex items-center gap-2 relative'>
|
||||
<div className='p-2 cursor-pointer' onClick={() => setToolbarOpen(!toolbarOpen)}>
|
||||
<PanelTopClose className={clsx('w-4 h-4', toolbarOpen && 'hidden')} />
|
||||
<PanelTopOpen className={clsx('w-4 h-4', !toolbarOpen && 'hidden')} />
|
||||
</div>
|
||||
<ToolbarContent open={toolbarOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user