generated from template/vite-react-template
temp
This commit is contained in:
87
src/main.tsx
87
src/main.tsx
@@ -1,87 +1,16 @@
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { App } from './App.tsx';
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import './index.css';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
import { Editor } from './pages/editor/index.tsx';
|
||||
import { ExampleApp } from './modules/panels/Example.tsx';
|
||||
import { page, app } from './routes';
|
||||
|
||||
const page = useContextKey('page');
|
||||
const wallnoteDom = useContextKey('wallnoteDom', () => {
|
||||
return document.getElementById('root');
|
||||
});
|
||||
const app = useContextKey<QueryRouterServer>('app');
|
||||
app
|
||||
.route({
|
||||
path: 'wallnote',
|
||||
key: 'getDomId',
|
||||
description: '获取墙记的dom',
|
||||
run: async (ctx) => {
|
||||
console.log('ctx', ctx);
|
||||
ctx.body = 'wallnoteDom';
|
||||
},
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
let root: Root | null = null;
|
||||
app
|
||||
.route({
|
||||
path: 'wallnote',
|
||||
key: 'getWallnoteReactDom',
|
||||
description: '获取墙记的react dom',
|
||||
run: async (ctx) => {
|
||||
const root = await useContextKey('wallReactRoot');
|
||||
if (!root) {
|
||||
ctx.throw(404, 'wallReactRoot not found');
|
||||
}
|
||||
ctx.body = 'wallReactRoot';
|
||||
},
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'wallnote',
|
||||
key: 'render',
|
||||
description: '渲染墙记',
|
||||
run: async (ctx) => {
|
||||
root = createRoot(wallnoteDom!);
|
||||
root.render(<App />);
|
||||
useContextKey('wallReactRoot', () => root, true);
|
||||
ctx.body = 'wallReactRoot';
|
||||
},
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
page.addPage('/note/:id', 'wallnote');
|
||||
page.subscribe(
|
||||
'wallnote',
|
||||
() => {
|
||||
root = createRoot(wallnoteDom!);
|
||||
root.render(<App />);
|
||||
},
|
||||
{ runImmediately: false },
|
||||
);
|
||||
|
||||
page.addPage('/editor', 'editor');
|
||||
page.subscribe(
|
||||
'editor',
|
||||
() => {
|
||||
root = createRoot(wallnoteDom!);
|
||||
root.render(<Editor />);
|
||||
},
|
||||
{ runImmediately: false },
|
||||
);
|
||||
|
||||
page.addPage('/panels', 'panels');
|
||||
page.addPage('/', 'wallnote');
|
||||
|
||||
setTimeout(() => {
|
||||
page.subscribe(
|
||||
'panels',
|
||||
'wallnote',
|
||||
() => {
|
||||
root = createRoot(wallnoteDom!);
|
||||
root.render(<ExampleApp />);
|
||||
app.call({
|
||||
path: 'wallnote',
|
||||
key: 'render',
|
||||
});
|
||||
},
|
||||
{ runImmediately: true },
|
||||
{ runImmediately: false },
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
41
src/modules/ReactRenderer.tsx
Normal file
41
src/modules/ReactRenderer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
export class ReactRenderer {
|
||||
component: any;
|
||||
element: HTMLElement;
|
||||
ref: React.RefObject<any>;
|
||||
props: any;
|
||||
root: any;
|
||||
constructor(component: any, { props, className }: any) {
|
||||
this.component = component;
|
||||
const el = document.createElement('div');
|
||||
this.element = el;
|
||||
this.ref = React.createRef();
|
||||
this.props = {
|
||||
...props,
|
||||
ref: this.ref,
|
||||
};
|
||||
el.className = className;
|
||||
this.root = createRoot(this.element);
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateProps(props: any) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.root.render(React.createElement(this.component, this.props));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactRenderer;
|
||||
4
src/modules/app.ts
Normal file
4
src/modules/app.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
|
||||
export const app = useContextKey<QueryRouterServer>('app');
|
||||
39
src/modules/editor/index.tsx
Normal file
39
src/modules/editor/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { TextEditor } from '@/modules/tiptap/editor';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type EditorProps = {
|
||||
className?: string;
|
||||
value?: string;
|
||||
id?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
export const Editor = ({ 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();
|
||||
};
|
||||
}, []);
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import WindowManager from './components/WindowManager';
|
||||
import { demoWindows } from './demo/DemoWindows';
|
||||
import './style.css';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePanelStore } from './store';
|
||||
import { useListenCmdB } from './hooks/use-listen-b';
|
||||
|
||||
export function ExampleApp() {
|
||||
const { data, toggleAICommand, init } = usePanelStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
data: state.data,
|
||||
toggleAICommand: state.toggleAICommand,
|
||||
init: state.init,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
init?.();
|
||||
}, [init]);
|
||||
useListenCmdB(() => {
|
||||
toggleAICommand?.();
|
||||
console.log('toggleAICommand');
|
||||
});
|
||||
return (
|
||||
<div className='h-screen w-screen overflow-hidden bg-gray-800'>
|
||||
<WindowManager windows={data?.windows || []} showTaskbar={data?.showTaskbar} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExampleApp;
|
||||
52
src/modules/panels/Panels.tsx
Normal file
52
src/modules/panels/Panels.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import WindowManager from './components/WindowManager';
|
||||
import { demoWindows } from './demo/DemoWindows';
|
||||
import './style.css';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePanelStore } from './store';
|
||||
import { useListenCmdB } from './hooks/use-listen-b';
|
||||
import { managerRender } from './render/main';
|
||||
console.log('managerRender', managerRender);
|
||||
export function Panels() {
|
||||
const ref = useRef<any>(null);
|
||||
const { data, toggleAICommand, init } = usePanelStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
data: state.data,
|
||||
toggleAICommand: state.toggleAICommand,
|
||||
init: state.init,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
init?.();
|
||||
}, [init]);
|
||||
useListenCmdB(() => {
|
||||
handleCommand();
|
||||
});
|
||||
const handleCommand = () => {
|
||||
const windows = ref.current?.getWindows();
|
||||
const newWindows = toggleAICommand?.(windows);
|
||||
// saveWindows?.(newWindows);
|
||||
ref.current?.setWindows(newWindows);
|
||||
console.log('toggleAICommand', newWindows);
|
||||
};
|
||||
useEffect(() => {
|
||||
console.log('data windows', data);
|
||||
}, [data]);
|
||||
return (
|
||||
<div className='h-screen w-screen overflow-hidden'>
|
||||
<WindowManager
|
||||
ref={ref}
|
||||
windows={data?.windows || []}
|
||||
// windows={demoWindows.slice(0, 2)}
|
||||
showTaskbar={data?.showTaskbar}
|
||||
onCommand={() => {
|
||||
handleCommand();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Panels;
|
||||
41
src/modules/panels/components/ReactRenderer.tsx
Normal file
41
src/modules/panels/components/ReactRenderer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
export class ReactRenderer {
|
||||
component: any;
|
||||
element: HTMLElement;
|
||||
ref: React.RefObject<any>;
|
||||
props: any;
|
||||
root: any;
|
||||
constructor(component: any, { props, className }: any) {
|
||||
this.component = component;
|
||||
const el = document.createElement('div');
|
||||
this.element = el;
|
||||
this.ref = React.createRef();
|
||||
this.props = {
|
||||
...props,
|
||||
ref: this.ref,
|
||||
};
|
||||
el.className = className;
|
||||
this.root = createRoot(this.element);
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateProps(props: any) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.root.render(React.createElement(this.component, this.props));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactRenderer;
|
||||
@@ -1,30 +1,32 @@
|
||||
import React, { useState, useCallback, useRef, useEffect, RefObject } from 'react';
|
||||
import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown } from 'lucide-react';
|
||||
import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown, CommandIcon } from 'lucide-react';
|
||||
import { WindowData, WindowPosition } from '../types';
|
||||
import classNames from 'clsx';
|
||||
import Draggable from 'react-draggable';
|
||||
import { ResizableBox } from 'react-resizable';
|
||||
import { getIconForWindowType } from './WindowIcons';
|
||||
import { useImperativeHandle } from 'react';
|
||||
import { emitter } from '../modules';
|
||||
interface WindowManagerProps {
|
||||
windows: WindowData[];
|
||||
showTaskbar?: boolean;
|
||||
onSave?: (windows: WindowData[]) => void;
|
||||
onCommand?: () => void;
|
||||
}
|
||||
|
||||
// Minimum window dimensions
|
||||
const MIN_WINDOW_WIDTH = 300;
|
||||
const MIN_WINDOW_HEIGHT = 200;
|
||||
|
||||
const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar = true, onSave }: WindowManagerProps, ref) => {
|
||||
const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar = true, onSave, onCommand }: WindowManagerProps, ref) => {
|
||||
const [windows, setWindows] = useState<WindowData[]>(initialWindows);
|
||||
const [minimizedWindows, setMinimizedWindows] = useState<string[]>([]);
|
||||
const [fullscreenWindow, setFullscreenWindow] = useState<string | null>(null);
|
||||
const [windowPositions, setWindowPositions] = useState<Record<string, WindowPosition>>({});
|
||||
const [activeWindow, setActiveWindow] = useState<string | null>(null);
|
||||
const [maxZIndex, setMaxZIndex] = useState(100);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [mount, setMount] = useState(false);
|
||||
const [update, setUpdate] = useState(0);
|
||||
// Create stable refs for each window
|
||||
const windowRefs = useRef<Record<string, React.RefObject<HTMLDivElement | null>>>({});
|
||||
const draggableRefs = useRef<Record<string, React.RefObject<HTMLDivElement | null>>>({});
|
||||
@@ -36,6 +38,11 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
getWindows: () => {
|
||||
return windows;
|
||||
},
|
||||
setWindows: (windows: WindowData[]) => {
|
||||
console.log('setWindows in manager', windows);
|
||||
setWindows(windows);
|
||||
setUpdate((prev) => prev + 1);
|
||||
},
|
||||
}));
|
||||
useEffect(() => {
|
||||
console.log('initialWindows', initialWindows);
|
||||
@@ -71,7 +78,7 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
setWindowPositions(positions);
|
||||
setMaxZIndex(1000 + windows.length);
|
||||
setMount(true);
|
||||
}, [windows.length]);
|
||||
}, [windows.length, update]);
|
||||
useEffect(() => {
|
||||
if (mount) {
|
||||
const newWindows = windows
|
||||
@@ -106,12 +113,20 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
const handleRemoveWindow = useCallback(
|
||||
(windowId: string) => {
|
||||
const window = windows.find((w) => w.id === windowId);
|
||||
if (window?.onHidden) {
|
||||
window.onHidden();
|
||||
const command = window?.commandList?.find((c) => c.key === 'close');
|
||||
if (command) {
|
||||
emitter.emit('window-command', { windowData: window, command });
|
||||
return;
|
||||
}
|
||||
setWindows((prev) => prev.filter((w) => w.id !== windowId));
|
||||
setMinimizedWindows((prev) => prev.filter((id) => id !== windowId));
|
||||
setWindows((prev) =>
|
||||
prev.map((w) => {
|
||||
if (w.id === windowId) {
|
||||
return { ...w, isMinimized: false };
|
||||
}
|
||||
return w;
|
||||
}),
|
||||
);
|
||||
if (fullscreenWindow === windowId) {
|
||||
setFullscreenWindow(null);
|
||||
}
|
||||
@@ -122,36 +137,44 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
// Handle window minimize
|
||||
const handleMinimizeWindow = useCallback(
|
||||
(windowId: string) => {
|
||||
if (minimizedWindows.includes(windowId)) {
|
||||
setMinimizedWindows((prev) => prev.filter((id) => id !== windowId));
|
||||
// Bring window to front when unminimizing
|
||||
bringToFront(windowId);
|
||||
} else {
|
||||
setMinimizedWindows((prev) => [...prev, windowId]);
|
||||
}
|
||||
let needBringToFront = false;
|
||||
setWindows((prev) =>
|
||||
prev.map((w) => {
|
||||
if (w.id === windowId) {
|
||||
needBringToFront = !w.isMinimized;
|
||||
return { ...w, isMinimized: !w.isMinimized };
|
||||
}
|
||||
return w;
|
||||
}),
|
||||
);
|
||||
|
||||
if (fullscreenWindow === windowId) {
|
||||
setFullscreenWindow(null);
|
||||
}
|
||||
|
||||
if (needBringToFront) {
|
||||
bringToFront(windowId);
|
||||
}
|
||||
},
|
||||
[minimizedWindows, fullscreenWindow],
|
||||
[, fullscreenWindow],
|
||||
);
|
||||
|
||||
// Handle window fullscreen
|
||||
const handleFullscreenWindow = useCallback(
|
||||
(windowId: string) => {
|
||||
setFullscreenWindow((prev) => (prev === windowId ? null : windowId));
|
||||
const handleFullscreenWindow = useCallback((windowId: string) => {
|
||||
setFullscreenWindow((prev) => (prev === windowId ? null : windowId));
|
||||
|
||||
// Ensure window is not minimized when going fullscreen
|
||||
if (minimizedWindows.includes(windowId)) {
|
||||
setMinimizedWindows((prev) => prev.filter((id) => id !== windowId));
|
||||
}
|
||||
|
||||
// Bring to front when going fullscreen
|
||||
bringToFront(windowId);
|
||||
},
|
||||
[minimizedWindows],
|
||||
);
|
||||
// Ensure window is not minimized when going fullscreen
|
||||
setWindows((prev) =>
|
||||
prev.map((w) => {
|
||||
if (w.id === windowId) {
|
||||
return { ...w, isMinimized: false };
|
||||
}
|
||||
return w;
|
||||
}),
|
||||
);
|
||||
// Bring to front when going fullscreen
|
||||
bringToFront(windowId);
|
||||
}, []);
|
||||
|
||||
// Bring window to front
|
||||
const bringToFront = useCallback(
|
||||
@@ -228,19 +251,19 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
// Render the taskbar with minimized windows
|
||||
const renderTaskbar = () => {
|
||||
const showWindowsList = windows.filter((window) => window.show && window.showTaskbar);
|
||||
if (showWindowsList.length === 0) return null;
|
||||
// if (showWindowsList.length === 0) return null;
|
||||
// useEffect(() => {
|
||||
// const handleResize = () => {
|
||||
// const icons = document.querySelectorAll('.more-icon');
|
||||
// icons.forEach((iconEl) => {
|
||||
// const icon = iconEl as HTMLElement;
|
||||
// const button = icon.closest('button');
|
||||
// if (button && button.offsetWidth <= 150) {
|
||||
// icon.style.display = 'none';
|
||||
// } else {
|
||||
// icon.style.display = 'block';
|
||||
// }
|
||||
// });
|
||||
// // const icons = document.querySelectorAll('.more-icon');
|
||||
// // icons.forEach((iconEl) => {
|
||||
// // const icon = iconEl as HTMLElement;
|
||||
// // const button = icon.closest('button');
|
||||
// // if (button && button.offsetWidth <= 150) {
|
||||
// // icon.style.display = 'none';
|
||||
// // } else {
|
||||
// // icon.style.display = 'block';
|
||||
// // }
|
||||
// // });
|
||||
// };
|
||||
|
||||
// window.addEventListener('resize', handleResize);
|
||||
@@ -251,9 +274,16 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
// };
|
||||
// }, []);
|
||||
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]'>
|
||||
<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]'>
|
||||
<div
|
||||
className='flex items-center space-x-2 cursor-pointer bg-blue-600 rounded-md p-1'
|
||||
onClick={() => {
|
||||
onCommand?.();
|
||||
}}>
|
||||
<CommandIcon size={16} />
|
||||
</div>
|
||||
{showWindowsList.map((window) => {
|
||||
const isMinimized = minimizedWindows.includes(window.id);
|
||||
const isMinimized = window.isMinimized;
|
||||
return (
|
||||
<button
|
||||
key={window.id}
|
||||
@@ -261,10 +291,11 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
'px-3 py-1 rounded text-sm max-w-[200px] truncate flex items-center justify-between',
|
||||
isMinimized ? 'bg-gray-600 hover:bg-gray-500' : 'bg-blue-600 hover:bg-blue-500',
|
||||
activeWindow === window.id && 'shadow-lg',
|
||||
'bar-button',
|
||||
'cursor-pointer',
|
||||
)}
|
||||
onClick={() => handleMinimizeWindow(window.id)}>
|
||||
<span className='truncate min-w-[16px]'>{window.title}</span>
|
||||
<span className='truncate min-w-[8px]'>{window.title}</span>
|
||||
<div className='flex items-center space-x-1 ml-2'>
|
||||
{/* {isMinimized ? <Maximize className='cursor-pointer more-icon' size={16} /> : <SquareMinus className='cursor-pointer more-icon' size={16} />} */}
|
||||
<ChevronDown className='cursor-pointer' size={16} />
|
||||
@@ -288,13 +319,12 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
|
||||
// Render a fixed position window
|
||||
const renderFixedWindow = (windowData: WindowData) => {
|
||||
const isMinimized = minimizedWindows.includes(windowData.id);
|
||||
const isMinimized = windowData.isMinimized;
|
||||
const isFullscreen = fullscreenWindow === windowData.id;
|
||||
const position = windowPositions[windowData.id];
|
||||
const Icon = getIconForWindowType(windowData.type || 'welcome');
|
||||
const showRounded = windowData.showRounded ?? true;
|
||||
if (!position) return null;
|
||||
if (isMinimized) return null;
|
||||
|
||||
// Convert width and height to numbers for Resizable component
|
||||
const width = isFullscreen ? window.innerWidth : position.width;
|
||||
@@ -310,21 +340,25 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
|
||||
const windowRef = windowRefs.current[windowData.id];
|
||||
const draggableRef = draggableRefs.current[windowData.id];
|
||||
|
||||
const zIndex = isFullscreen ? 9999 : windowData.id == '__ai__' ? 3000 : position.zIndex;
|
||||
return (
|
||||
<div
|
||||
key={windowData.id}
|
||||
className={classNames('absolute pointer-events-auto', windowData.show && 'block', !windowData.show && 'hidden')}
|
||||
className={classNames(
|
||||
'absolute pointer-events-auto', //
|
||||
windowData.show && !isMinimized && 'block',
|
||||
(!windowData.show || isMinimized) && 'hidden',
|
||||
)}
|
||||
style={{
|
||||
left: isFullscreen ? 0 : position.x,
|
||||
top: isFullscreen ? 0 : position.y,
|
||||
width: width,
|
||||
height: height,
|
||||
zIndex: isFullscreen ? 9999 : position.zIndex,
|
||||
zIndex: zIndex,
|
||||
}}
|
||||
ref={windowRef}>
|
||||
<div
|
||||
className={classNames('window-container', isFullscreen && 'fullscreen')}
|
||||
className={classNames('window-container', isFullscreen && 'fullscreen', showTaskbar && 'hidden-taskbar', windowData.show && !isMinimized && 'block')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@@ -383,7 +417,7 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
</div>
|
||||
<div className='window-controls'>{renderWindowControls(windowData.id)}</div>
|
||||
</div>
|
||||
<div className='window-content h-[calc(100%-32px)] overflow-auto p-4'>
|
||||
<div className='window-content h-[calc(100%-32px)] overflow-auto'>
|
||||
<div className='h-full flex flex-col'>
|
||||
<WindowContent window={windowData} />
|
||||
</div>
|
||||
@@ -396,7 +430,6 @@ const WindowManager = React.forwardRef(({ windows: initialWindows, showTaskbar =
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='h-screen w-screen overflow-hidden' ref={containerRef}>
|
||||
{windows.map((window) => renderFixedWindow(window))}
|
||||
@@ -409,10 +442,10 @@ export const WindowContent = React.memo((props: { window: WindowData }) => {
|
||||
const { window } = props;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// 获取属性,判断是否加载对应的应用
|
||||
}
|
||||
console.log('window editor render', window);
|
||||
emitter.emit('window-load', { windowData: window, el: ref.current });
|
||||
return () => {
|
||||
emitter.emit('window-unload', { windowData: window, el: ref.current });
|
||||
};
|
||||
}, []);
|
||||
return <div data-id={window.id} className='flex-1 overflow-auto editor-window' ref={ref}></div>;
|
||||
});
|
||||
|
||||
25
src/modules/panels/components/content/SplitPanel.tsx
Normal file
25
src/modules/panels/components/content/SplitPanel.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import clsx from 'clsx';
|
||||
import { useRef } from 'react';
|
||||
export const SplitPanel = (porps: any) => {
|
||||
const { direction, headerHeight, showHeader, chatBoxHeight, showChatBox, bodyHeight, showFooter, footerClassName, className } = porps;
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<PanelGroup autoSaveId='example' direction={direction} className='w-full h-full editor-container relative'>
|
||||
<Panel minSize={10} defaultSize={headerHeight} className={clsx('editor-header h-10', showHeader ? 'block' : 'hidden')}>
|
||||
<div className='w-full h-full'>{/* editor-header */}</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle className={clsx('editor-resize-handle border-gray-300 border-1', showHeader ? 'block' : 'hidden')} />
|
||||
<Panel minSize={10} defaultSize={chatBoxHeight} className={clsx('editor-chat-box h-66', showChatBox ? 'block' : 'hidden')}>
|
||||
<div className='w-full h-full'>{/* editor-chat-box */}</div>
|
||||
</Panel>
|
||||
<PanelResizeHandle className={clsx('editor-resize-handle border-gray-300 border-1', showChatBox ? 'block' : 'hidden')} />
|
||||
<Panel minSize={10} defaultSize={bodyHeight}>
|
||||
<div ref={editorRef} className={clsx('w-full h-full node-editor', className)}></div>
|
||||
</Panel>
|
||||
<Panel className={clsx('editor-footer h-10', showFooter ? 'block' : 'hidden', footerClassName)}>
|
||||
<div className='w-full h-full'>{/* editor-footer */}</div>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WindowData } from '../types';
|
||||
|
||||
export const createEditorWindow = (data: any): WindowData => {
|
||||
export const createDemoEditorWindow = (data: any): WindowData => {
|
||||
return {
|
||||
...data,
|
||||
showTitle: true,
|
||||
@@ -104,17 +104,17 @@ const windowPositions = {
|
||||
|
||||
// Demo windows data using the createEditorWindow function
|
||||
export const demoWindows: WindowData[] = [
|
||||
createEditorWindow({ title: 'Welcome', id: 'window1', type: 'welcome' }),
|
||||
createEditorWindow({ title: 'Image Viewer', id: 'window2', type: 'image' }),
|
||||
createEditorWindow({ title: 'Text Editor', id: 'window3', type: 'document' }),
|
||||
createEditorWindow({ title: 'Calculator', id: 'window4', type: 'calculator' }),
|
||||
createEditorWindow({ title: 'Code Editor', id: 'code-editor', type: 'code' }),
|
||||
createEditorWindow({ title: 'Document', id: 'document', type: 'document' }),
|
||||
createEditorWindow({ title: 'Analytics', id: 'analytics', type: 'analytics' }),
|
||||
createEditorWindow({ title: 'Settings', id: 'settings', type: 'settings' }),
|
||||
createEditorWindow({ title: 'Layers', id: 'layers', type: 'layers' }),
|
||||
createEditorWindow({ title: 'Database', id: 'database', type: 'database' }),
|
||||
createEditorWindow({ title: 'Server', id: 'server', type: 'server' }),
|
||||
createEditorWindow({ title: 'Terminal', id: 'terminal', type: 'terminal' }),
|
||||
createEditorWindow({ title: 'Command', id: 'command', type: 'command' }),
|
||||
createDemoEditorWindow({ title: 'Welcome', id: 'window1', type: 'welcome' }),
|
||||
createDemoEditorWindow({ title: 'Image Viewer', id: 'window2', type: 'image' }),
|
||||
createDemoEditorWindow({ title: 'Text Editor', id: 'window3', type: 'document' }),
|
||||
createDemoEditorWindow({ title: 'Calculator', id: 'window4', type: 'calculator' }),
|
||||
createDemoEditorWindow({ title: 'Code Editor', id: 'code-editor', type: 'code' }),
|
||||
createDemoEditorWindow({ title: 'Document', id: 'document', type: 'document' }),
|
||||
createDemoEditorWindow({ title: 'Analytics', id: 'analytics', type: 'analytics' }),
|
||||
createDemoEditorWindow({ title: 'Settings', id: 'settings', type: 'settings' }),
|
||||
createDemoEditorWindow({ title: 'Layers', id: 'layers', type: 'layers' }),
|
||||
createDemoEditorWindow({ title: 'Database', id: 'database', type: 'database' }),
|
||||
createDemoEditorWindow({ title: 'Server', id: 'server', type: 'server' }),
|
||||
createDemoEditorWindow({ title: 'Terminal', id: 'terminal', type: 'terminal' }),
|
||||
createDemoEditorWindow({ title: 'Command', id: 'command', type: 'command' }),
|
||||
].map((window) => ({ ...window, position: windowPositions[window.id] }));
|
||||
|
||||
@@ -5,7 +5,7 @@ export const useListenCmdB = (callback: () => void) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Check for Command key on macOS
|
||||
if (isMac ? event.metaKey && event.key === 'b' : event.ctrlKey && event.key === 'b') {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'b') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import './style.css';
|
||||
|
||||
export { Panels } from './Panels';
|
||||
5
src/modules/panels/modules/index.ts
Normal file
5
src/modules/panels/modules/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
export const emitter = useContextKey<EventEmitter>('emitter', () => {
|
||||
return new EventEmitter();
|
||||
});
|
||||
9
src/modules/panels/render/main.ts
Normal file
9
src/modules/panels/render/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { emitter } from '../modules/index';
|
||||
import { ManagerRender } from './manager/manager';
|
||||
export { emitter };
|
||||
export { useContextKey };
|
||||
|
||||
export const managerRender = useContextKey<ManagerRender>('managerRender', () => {
|
||||
return new ManagerRender();
|
||||
});
|
||||
111
src/modules/panels/render/manager/manager.ts
Normal file
111
src/modules/panels/render/manager/manager.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { WindowData } from '../../types';
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { ReactRenderer } from '../../components/ReactRenderer';
|
||||
import { BaseLoad } from '@kevisual/system-lib/dist/load';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
|
||||
export const emitter = useContextKey<EventEmitter>('emitter', () => {
|
||||
return new EventEmitter();
|
||||
});
|
||||
// const load = new BaseLoad();
|
||||
|
||||
class HtmlRender {
|
||||
render({ renderRoot, data }: any) {
|
||||
const div = `<div id="${data.id}">${data.title}</div>`;
|
||||
renderRoot.appendChild(div);
|
||||
}
|
||||
destroy() {
|
||||
// 什么也不做
|
||||
}
|
||||
}
|
||||
export class BaseRender {
|
||||
/**
|
||||
* 在页面当中是否加载
|
||||
* */
|
||||
isLoaded = false;
|
||||
status = 'loading';
|
||||
element?: HTMLElement;
|
||||
windowData?: WindowData;
|
||||
render?: ReactRenderer | HtmlRender;
|
||||
type?: 'react' | 'html';
|
||||
async load(windowData: WindowData, element: HTMLElement) {
|
||||
this.isLoaded = true;
|
||||
this.windowData = windowData;
|
||||
this.status = 'loaded';
|
||||
this.element = element;
|
||||
|
||||
// @ts-ignore
|
||||
const app = (await useContextKey('app')) as QueryRouterServer;
|
||||
const render = windowData.render;
|
||||
if (render?.command) {
|
||||
const res = await app.call({
|
||||
path: render.command.path,
|
||||
key: render.command.key,
|
||||
payload: render.command.payload,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const { lib, type } = res.body;
|
||||
if (type === 'react') {
|
||||
const ReactNode = lib;
|
||||
// 这是一个打开后就不需要再管理的组件
|
||||
const renderNote = new ReactRenderer(ReactNode, {
|
||||
props: {
|
||||
id: windowData.id,
|
||||
windowData: windowData,
|
||||
},
|
||||
className: 'w-full h-full',
|
||||
});
|
||||
this.render = renderNote;
|
||||
this.element.appendChild(renderNote.element);
|
||||
} else if (type === 'html') {
|
||||
this.render = new lib() as HtmlRender;
|
||||
this.render.render({
|
||||
renderRoot: this.element,
|
||||
data: windowData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async unload(windowData: WindowData) {
|
||||
this.isLoaded = false;
|
||||
if (this.type === 'react') {
|
||||
this.render?.destroy();
|
||||
} else if (this.type === 'html') {
|
||||
this.render?.destroy();
|
||||
}
|
||||
this.status = 'loading';
|
||||
this.windowData = undefined;
|
||||
this.element = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class ManagerRender {
|
||||
constructor() {
|
||||
const that = this;
|
||||
emitter.on('window-load', (data: { windowData: WindowData; el: HTMLElement }) => {
|
||||
that.load(data.windowData, data.el);
|
||||
});
|
||||
emitter.on('window-unload', (windowData: WindowData) => {
|
||||
that.unload(windowData);
|
||||
});
|
||||
}
|
||||
renders = new Map<string, BaseRender>();
|
||||
load(windowData: WindowData, element: HTMLElement) {
|
||||
const id = windowData.id;
|
||||
if (this.renders.has(id)) {
|
||||
this.renders.get(id)?.load(windowData, element);
|
||||
} else {
|
||||
const render = new BaseRender();
|
||||
this.renders.set(id, render);
|
||||
render.load(windowData, element);
|
||||
}
|
||||
}
|
||||
unload(windowData: WindowData) {
|
||||
const id = windowData.id;
|
||||
if (this.renders.has(id)) {
|
||||
this.renders.get(id)?.unload(windowData);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/modules/panels/routes.ts
Normal file
42
src/modules/panels/routes.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
import { usePanelStore } from './store';
|
||||
import { createEditorWindow } from './store/create/create-editor-window';
|
||||
export const app = useContextKey<QueryRouterServer>('app');
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'panels',
|
||||
key: 'add-editor-window',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const { data } = ctx.query;
|
||||
const state = usePanelStore.getState();
|
||||
|
||||
const newWindow = createEditorWindow(data.pageId, data.nodeData, {
|
||||
id: data.nodeData.id,
|
||||
title: data.nodeData.title,
|
||||
show: true,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 600,
|
||||
height: 400,
|
||||
zIndex: 1000,
|
||||
},
|
||||
});
|
||||
state.setEditorWindow(newWindow.windowData);
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'panels',
|
||||
key: 'close-editor-window',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const { data } = ctx.query;
|
||||
const state = usePanelStore.getState();
|
||||
state.closeEditorWindow(data.id);
|
||||
})
|
||||
.addTo(app);
|
||||
56
src/modules/panels/store/create/create-editor-window.ts
Normal file
56
src/modules/panels/store/create/create-editor-window.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { WindowData } from '../../types';
|
||||
import { getDocumentWidthAndHeight } from '../../utils/document-width';
|
||||
|
||||
/**
|
||||
* 创建编辑器窗口
|
||||
* @param id 整个页面的的id
|
||||
* @param nodeData 节点数据
|
||||
* @param windowData 窗口数据
|
||||
* @returns
|
||||
*/
|
||||
export const createEditorWindow = (pageId: string, nodeData: any, windowData?: WindowData) => {
|
||||
const { width, height } = getDocumentWidthAndHeight();
|
||||
return {
|
||||
nodeData,
|
||||
windowData: {
|
||||
id: nodeData.id,
|
||||
type: 'editor',
|
||||
title: nodeData.title || '编辑器',
|
||||
showTitle: true,
|
||||
showRounded: true,
|
||||
showTaskbar: true,
|
||||
showMoreTools: true,
|
||||
defaultPosition: {
|
||||
x: width - 1000,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
height: height,
|
||||
zIndex: 1000,
|
||||
},
|
||||
moreTools: [
|
||||
{
|
||||
command: {
|
||||
path: 'window',
|
||||
key: 'close',
|
||||
payload: {
|
||||
id: nodeData.id,
|
||||
},
|
||||
},
|
||||
title: '关闭',
|
||||
key: 'close',
|
||||
},
|
||||
],
|
||||
render: {
|
||||
command: {
|
||||
path: 'editor',
|
||||
key: 'render',
|
||||
payload: {
|
||||
pageId: pageId,
|
||||
id: nodeData.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
...windowData,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,8 @@ import { query } from '@/modules/query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getDocumentWidthAndHeight } from '../utils/document-width';
|
||||
import { produce } from 'immer';
|
||||
import { createEditorWindow } from './create/create-editor-window';
|
||||
import { createDemoEditorWindow } from '../demo/DemoWindows';
|
||||
|
||||
interface PanelStore {
|
||||
data?: PanelData;
|
||||
@@ -12,7 +14,10 @@ interface PanelStore {
|
||||
init?: (id?: string) => Promise<any>;
|
||||
id: string;
|
||||
setId: (id: string) => void;
|
||||
toggleAICommand: () => void;
|
||||
toggleAICommand: (windows: WindowData[]) => WindowData[];
|
||||
saveWindows: (windows: WindowData[]) => void;
|
||||
setEditorWindow: (windowData: WindowData) => void;
|
||||
closeEditorWindow: (id: string) => void;
|
||||
}
|
||||
interface PanelData {
|
||||
/**
|
||||
@@ -22,7 +27,7 @@ interface PanelData {
|
||||
/**
|
||||
* 是否显示任务栏
|
||||
*/
|
||||
showTaskbar: boolean;
|
||||
showTaskbar?: boolean;
|
||||
}
|
||||
|
||||
export const usePanelStore = create<PanelStore>((set, get) => ({
|
||||
@@ -39,60 +44,91 @@ export const usePanelStore = create<PanelStore>((set, get) => ({
|
||||
},
|
||||
|
||||
init: async (id?: string) => {
|
||||
const cache = new MyCache<PanelData>(id || 'workspace');
|
||||
if (id) {
|
||||
// id存在,则获取本地和获取远程,进行对比,如果需要更新,则更新
|
||||
if (cache.data) {
|
||||
const updatedAt = cache.updatedAt;
|
||||
const res = await query.post({ path: 'workspace', key: 'env', id, updatedAt });
|
||||
if (res.code === 200) {
|
||||
const newData = res.data;
|
||||
if (newData) {
|
||||
cache.setData(newData);
|
||||
set({
|
||||
data: newData,
|
||||
id: id,
|
||||
});
|
||||
} else {
|
||||
set({ data: cache.data, id: id });
|
||||
}
|
||||
} else {
|
||||
toast.error('获取环境失败');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const res = await query.post({ path: 'workspace', key: 'env', id });
|
||||
if (res.code === 200) {
|
||||
const newData = res.data;
|
||||
if (newData) {
|
||||
cache.setData(newData);
|
||||
set({
|
||||
data: newData,
|
||||
id: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cache.data) {
|
||||
set({
|
||||
data: cache.data,
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
data: { windows: [], showTaskbar: true },
|
||||
});
|
||||
}
|
||||
// const cache = new MyCache<PanelData>(id || 'workspace');
|
||||
// if (id) {
|
||||
// // id存在,则获取本地和获取远程,进行对比,如果需要更新,则更新
|
||||
// if (cache.data) {
|
||||
// const updatedAt = cache.updatedAt;
|
||||
// const res = await query.post({ path: 'workspace', key: 'env', id, updatedAt });
|
||||
// if (res.code === 200) {
|
||||
// const newData = res.data;
|
||||
// if (newData) {
|
||||
// cache.setData(newData);
|
||||
// set({
|
||||
// data: newData,
|
||||
// id: id,
|
||||
// });
|
||||
// } else {
|
||||
// set({ data: cache.data, id: id });
|
||||
// }
|
||||
// } else {
|
||||
// toast.error('获取环境失败');
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// const res = await query.post({ path: 'workspace', key: 'env', id });
|
||||
// if (res.code === 200) {
|
||||
// const newData = res.data;
|
||||
// if (newData) {
|
||||
// cache.setData(newData);
|
||||
// set({
|
||||
// data: newData,
|
||||
// id: id,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } else if (cache.data) {
|
||||
// set({
|
||||
// data: cache.data,
|
||||
// });
|
||||
// } else {
|
||||
// set({
|
||||
// data: { windows: [], showTaskbar: true },
|
||||
// });
|
||||
// }
|
||||
|
||||
set({
|
||||
data: {
|
||||
windows: [e.windowData],
|
||||
showTaskbar: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
toggleAICommand: () => {
|
||||
setEditorWindow: (windowData: WindowData) => {
|
||||
const { data } = get();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const has = data.windows.find((w) => w.id === windowData.id);
|
||||
if (has) {
|
||||
data.windows = data.windows.map((w) => {
|
||||
if (w.id === windowData.id) {
|
||||
return windowData;
|
||||
}
|
||||
return w;
|
||||
});
|
||||
} else {
|
||||
data.windows.push(windowData);
|
||||
}
|
||||
console.log('data', data);
|
||||
set({ data: { ...data, windows: data.windows } });
|
||||
},
|
||||
toggleAICommand: (windows: WindowData[]) => {
|
||||
// const { data } = get();
|
||||
// if (!data) {
|
||||
// return;
|
||||
// }
|
||||
const data = { windows };
|
||||
const has = data.windows.find((w) => w.id === '__ai__');
|
||||
if (has) {
|
||||
data.windows = data.windows.map((w) => {
|
||||
if (w.id === '__ai__') {
|
||||
return { ...w, show: !w.show };
|
||||
console.log('w', w.isMinimized, w.show);
|
||||
if (w.isMinimized || !w.show) {
|
||||
return { ...w, show: true, isMinimized: false };
|
||||
}
|
||||
return { ...w, show: !w.show, isMinimized: false };
|
||||
}
|
||||
return w;
|
||||
});
|
||||
@@ -101,10 +137,10 @@ export const usePanelStore = create<PanelStore>((set, get) => ({
|
||||
data.windows.push({
|
||||
id: '__ai__',
|
||||
title: 'AI Command',
|
||||
type: 'commandƒ',
|
||||
type: 'command',
|
||||
position: {
|
||||
x: 100,
|
||||
y: height - 200,
|
||||
y: height - 200 - 40,
|
||||
width: width - 200,
|
||||
height: 200,
|
||||
zIndex: 1000,
|
||||
@@ -113,7 +149,37 @@ export const usePanelStore = create<PanelStore>((set, get) => ({
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
set({ data: { ...data, windows: data.windows } });
|
||||
// set({ data: { ...data, windows: data.windows } });
|
||||
console.log('data', data);
|
||||
return data.windows;
|
||||
},
|
||||
saveWindows: (windows: WindowData[]) => {
|
||||
set({ data: { ...get().data, windows } });
|
||||
},
|
||||
closeEditorWindow: (id: string) => {
|
||||
const { data } = get();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
data.windows = data.windows.filter((w) => w.id !== id);
|
||||
set({ data: { ...data, windows: data.windows } });
|
||||
},
|
||||
}));
|
||||
|
||||
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);
|
||||
|
||||
@@ -22,8 +22,12 @@
|
||||
right: 0 !important;
|
||||
bottom: 40px !important; /* Leave space for taskbar */
|
||||
width: 100% !important;
|
||||
height: calc(100% - 40px) !important;
|
||||
z-index: 9999 !important;
|
||||
height: calc(100% - 40px);
|
||||
z-index: 9900 !important;
|
||||
}
|
||||
|
||||
.fullscreen.hidden-taskbar {
|
||||
height: calc(100% - 0px) !important;
|
||||
}
|
||||
|
||||
/* Resize handles */
|
||||
|
||||
@@ -6,6 +6,19 @@ export interface WindowPosition {
|
||||
height: number;
|
||||
zIndex: number;
|
||||
}
|
||||
export type WindowCommand = {
|
||||
path: string;
|
||||
key?: string;
|
||||
payload?: any;
|
||||
};
|
||||
export type WindowCommandData = {
|
||||
command: WindowCommand;
|
||||
title: string;
|
||||
key: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
onClick?: WindowCommand;
|
||||
};
|
||||
export interface WindowData {
|
||||
// 窗口的唯一标识
|
||||
id: string;
|
||||
@@ -34,18 +47,13 @@ export interface WindowData {
|
||||
// 是否显示更多工具
|
||||
showMoreTools?: boolean;
|
||||
// 更多工具
|
||||
moreTools?: MoreTool[];
|
||||
// 当隐藏窗口存在,只关闭隐藏窗口,不退出程序
|
||||
onHidden?: () => void;
|
||||
}
|
||||
export interface MoreTool {
|
||||
// 工具的名称
|
||||
title?: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
key?: string;
|
||||
// 工具的图标
|
||||
icon?: string;
|
||||
// 工具的点击事件
|
||||
onClick?: () => void;
|
||||
moreTools?: WindowCommandData[];
|
||||
// 工具列表
|
||||
commandList?: WindowCommandData[];
|
||||
// 渲染
|
||||
render?: {
|
||||
command: WindowCommand;
|
||||
props?: any;
|
||||
className?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Typography from '@tiptap/extension-typography';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { Commands, getSuggestionItems, createSuggestionConfig } from './extensions/suggestions';
|
||||
import { Commands, getSuggestionItems, createSuggestionConfig, CommandItem } from './extensions/suggestions';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
import 'highlight.js/styles/github.css';
|
||||
@@ -28,22 +28,28 @@ lowlight.register('markdown', markdown);
|
||||
|
||||
export class TextEditor {
|
||||
private editor?: Editor;
|
||||
private opts?: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void };
|
||||
private element?: HTMLElement;
|
||||
private isInitialSetup: boolean = true;
|
||||
|
||||
constructor() {}
|
||||
createEditor(el: HTMLElement, opts?: { markdown?: string; html?: string }) {
|
||||
createEditor(el: HTMLElement, opts?: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }) {
|
||||
if (this.editor) {
|
||||
this.destroy();
|
||||
}
|
||||
this.opts = opts;
|
||||
this.element = el;
|
||||
const html = opts?.html || '';
|
||||
const items = getSuggestionItems();
|
||||
const items = opts?.items || getSuggestionItems();
|
||||
const suggestionConfig = createSuggestionConfig(items);
|
||||
this.isInitialSetup = true;
|
||||
this.editor = new Editor({
|
||||
element: el, // 指定编辑器容器
|
||||
extensions: [
|
||||
StarterKit, // 使用 StarterKit 包含基础功能
|
||||
Highlight,
|
||||
Placeholder.configure({
|
||||
placeholder: 'Type ! to see commands (e.g., !today, !list, !good)...',
|
||||
placeholder: 'Type ! to see commands (e.g., !today, !list !test )...',
|
||||
}),
|
||||
Typography,
|
||||
Markdown,
|
||||
@@ -81,11 +87,37 @@ export class TextEditor {
|
||||
suggestion: suggestionConfig,
|
||||
}),
|
||||
],
|
||||
content: html, // 初始化内容
|
||||
content: html, // 初始化内容,
|
||||
onUpdate: () => {
|
||||
if (this.isInitialSetup) {
|
||||
this.isInitialSetup = false;
|
||||
return;
|
||||
}
|
||||
if (this.opts?.onUpdateHtml) {
|
||||
this.opts.onUpdateHtml(this.editor?.getHTML() || '');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
setContent(html: string) {
|
||||
this.editor?.commands.setContent(html);
|
||||
updateSugestionConfig(items: CommandItem[]) {
|
||||
if (!this.element) return;
|
||||
const element = this.element;
|
||||
if (this.editor) {
|
||||
const content = this.editor.getHTML(); // Save current content
|
||||
const opts = { ...this.opts, html: content, items };
|
||||
this.createEditor(element, opts); // Recreate the editor with the new config
|
||||
}
|
||||
}
|
||||
setContent(html: string, emitUpdate?: boolean) {
|
||||
this.editor?.commands.setContent(html, emitUpdate);
|
||||
}
|
||||
/**
|
||||
* before set options ,you should has element and editor
|
||||
* @param opts
|
||||
*/
|
||||
setOptions(opts: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }) {
|
||||
this.opts = { ...this.opts, ...opts };
|
||||
this.createEditor(this.element!, this.opts!);
|
||||
}
|
||||
getHtml() {
|
||||
return this.editor?.getHTML();
|
||||
@@ -93,12 +125,6 @@ export class TextEditor {
|
||||
getContent() {
|
||||
return this.editor?.getText();
|
||||
}
|
||||
onContentChange(callback: (html: string) => void) {
|
||||
this.editor?.off('update'); // 移除之前的监听
|
||||
this.editor?.on('update', () => {
|
||||
callback(this.editor?.getHTML() || '');
|
||||
});
|
||||
}
|
||||
foucus() {
|
||||
this.editor?.view?.focus?.();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Commands = Extension.create({
|
||||
suggestion: {
|
||||
char: '!',
|
||||
command: ({ editor, range, props }: any) => {
|
||||
console.log('sdfsd')
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
|
||||
@@ -60,7 +60,7 @@ export const createSuggestionConfig = (items: CommandItem[]) => {
|
||||
popup = document.createElement('div');
|
||||
popup.className = 'commands-popup';
|
||||
popup.style.position = 'fixed'; // Use fixed instead of absolute for better viewport positioning
|
||||
popup.style.zIndex = '1000';
|
||||
popup.style.zIndex = '9999';
|
||||
document.body.appendChild(popup);
|
||||
|
||||
popup.appendChild(component.element);
|
||||
|
||||
87
src/pages/editor/NodeTextEditor.tsx
Normal file
87
src/pages/editor/NodeTextEditor.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { TextEditor } from '@/modules/tiptap/editor';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useWallStore } from '../wall/store/wall';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { app } from '../editor/app';
|
||||
|
||||
/**
|
||||
* 监听ctrl+s保存内容
|
||||
* esc退出编辑
|
||||
* @param saveContent 保存内容
|
||||
* @param exitEdit 退出编辑
|
||||
*/
|
||||
export const useListenCtrlS = (saveContent: () => void, exitEdit: () => void) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
saveContent();
|
||||
} else if (event.key === 'Escape') {
|
||||
exitEdit();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [saveContent]);
|
||||
};
|
||||
type EditorProps = {
|
||||
id?: string;
|
||||
};
|
||||
export const NodeTextEditor = ({ id }: EditorProps) => {
|
||||
const textEditorRef = useRef<TextEditor | null>(null);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const { getNodeById, saveNodeById } = useWallStore(useShallow((state) => ({ getNodeById: state.getNodeById, saveNodeById: state.saveNodeById })));
|
||||
useEffect(() => {
|
||||
const editor = new TextEditor();
|
||||
textEditorRef.current = editor;
|
||||
editor.createEditor(editorRef.current!, { html: '' });
|
||||
getIdContent();
|
||||
return () => {
|
||||
editor.destroy();
|
||||
};
|
||||
}, []);
|
||||
const getIdContent = async () => {
|
||||
if (!id) return;
|
||||
const node = await getNodeById(id);
|
||||
if (node) {
|
||||
textEditorRef.current?.setContent(node.data.html);
|
||||
}
|
||||
};
|
||||
const saveContent = async () => {
|
||||
if (!id) return;
|
||||
const html = await textEditorRef.current?.getHtml();
|
||||
if (html) {
|
||||
saveNodeById(id, {
|
||||
html: html,
|
||||
});
|
||||
}
|
||||
};
|
||||
const exitEdit = () => {
|
||||
// 退出编辑
|
||||
saveContent()
|
||||
setTimeout(() => {
|
||||
app.call({
|
||||
path: 'panels',
|
||||
key: 'close-editor-window',
|
||||
payload: {
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
useListenCtrlS(saveContent, exitEdit);
|
||||
|
||||
return (
|
||||
<div className={clsx('w-full h-full relative')}>
|
||||
<div ref={editorRef} className={clsx('w-full h-full node-editor')}></div>
|
||||
<div className='absolute top-2 right-2 cursor-pointer' onClick={() => saveContent()}>
|
||||
<Save />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
src/pages/editor/app.ts
Normal file
5
src/pages/editor/app.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
|
||||
export const app = useContextKey<QueryRouterServer>('app');
|
||||
|
||||
@@ -1,32 +1,2 @@
|
||||
import { TextEditor } from '@/modules/tiptap/editor';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
type EditorProps = {
|
||||
className?: string;
|
||||
value?: string;
|
||||
id?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
export const Editor = ({ 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 });
|
||||
editor.onContentChange((content) => {
|
||||
onChange?.(content);
|
||||
});
|
||||
setMount(true);
|
||||
return () => {
|
||||
editor.destroy();
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (textEditorRef.current && id && mount) {
|
||||
textEditorRef.current.setContent(value || '');
|
||||
}
|
||||
}, [id, mount]);
|
||||
return <div ref={editorRef} className={clsx('w-full h-full node-editor', className)}></div>;
|
||||
};
|
||||
import { Editor } from '@/modules/editor';
|
||||
export { Editor };
|
||||
|
||||
6
src/pages/wall/app.ts
Normal file
6
src/pages/wall/app.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
|
||||
|
||||
export const app = useContextKey<QueryRouterServer>('app');
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
}
|
||||
}
|
||||
.tiptap {
|
||||
margin: 0.5rem 1rem;
|
||||
/* margin: 0.5rem 1rem; */
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useWallStore } from '../store/wall';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { toast } from 'react-toastify';
|
||||
import { message } from '@/modules/message';
|
||||
import { app } from '../app';
|
||||
import hljs from 'highlight.js';
|
||||
import { Edit } from 'lucide-react';
|
||||
export type WallData<T = Record<string, any>> = {
|
||||
@@ -50,6 +51,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
const wallStore = useWallStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
id: state.id,
|
||||
setSelectedNode: state.setSelectedNode,
|
||||
saveNodes: state.saveNodes,
|
||||
checkAndOpen: state.checkAndOpen,
|
||||
@@ -83,17 +85,6 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
},
|
||||
};
|
||||
});
|
||||
// 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 = {};
|
||||
@@ -102,19 +93,30 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
||||
const showOpen = () => {
|
||||
const node = store.getNode(props.id);
|
||||
console.log('node eidt', node);
|
||||
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('节点不存在');
|
||||
}
|
||||
app.call({
|
||||
path: 'panels',
|
||||
key: 'add-editor-window',
|
||||
payload: {
|
||||
data: {
|
||||
pageId: wallStore.id || 'local-browser',
|
||||
type: 'wallnote',
|
||||
nodeData: node,
|
||||
},
|
||||
},
|
||||
});
|
||||
// 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);
|
||||
return (
|
||||
|
||||
@@ -57,6 +57,8 @@ interface WallState {
|
||||
clearId: () => Promise<void>;
|
||||
mouseSelect: boolean;
|
||||
setMouseSelect: (mouseSelect: boolean) => void;
|
||||
getNodeById: (id: string) => Promise<NodeData | null>;
|
||||
saveNodeById: (id: string, data: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useWallStore = create<WallState>((set, get) => ({
|
||||
@@ -72,7 +74,6 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
if (!get().id) {
|
||||
const covertData = getNodeData(nodes);
|
||||
setWallData({ nodes: covertData });
|
||||
showMessage && message.success('保存到本地');
|
||||
} else {
|
||||
const { id } = get();
|
||||
const userWallStore = useUserWallStore.getState();
|
||||
@@ -201,4 +202,28 @@ export const useWallStore = create<WallState>((set, get) => ({
|
||||
},
|
||||
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;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
set({
|
||||
nodes: newNodes,
|
||||
});
|
||||
get().saveNodes(newNodes, { showMessage: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
73
src/routes.tsx
Normal file
73
src/routes.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App.tsx';
|
||||
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 { Panels } from './modules/panels/index.tsx';
|
||||
import { Page } from '@kevisual/system-lib/dist/web-page';
|
||||
|
||||
import './modules/panels/routes.ts';
|
||||
|
||||
export const page = useContextKey<Page>('page');
|
||||
export const app = useContextKey<QueryRouterServer>('app');
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'wallnote',
|
||||
key: 'render',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const rootEl = document.getElementById('root') as HTMLElement;
|
||||
const root = createRoot(rootEl);
|
||||
useContextKey('wallnoteRoot', () => root, true);
|
||||
root.render(<App />);
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'wallnote',
|
||||
key: 'lib',
|
||||
description: '获取编辑器',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
ctx.body = { Panels };
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'editor',
|
||||
key: 'render',
|
||||
description: '获取编辑器',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
ctx.body = { lib: NodeTextEditor, type: 'react', Panels };
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'editor',
|
||||
key: 'render2',
|
||||
description: '获取编辑器',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
class HtmlRender {
|
||||
render({ renderRoot, data }: any) {
|
||||
const newDivStr = `<div id="${data.id}">${data.title}</div>`;
|
||||
const newDiv = document.createElement('div');
|
||||
newDiv.innerHTML = newDivStr;
|
||||
renderRoot.appendChild(newDiv);
|
||||
}
|
||||
destroy() {
|
||||
// 什么也不做
|
||||
}
|
||||
}
|
||||
ctx.body = {
|
||||
lib: HtmlRender,
|
||||
type: 'html',
|
||||
};
|
||||
})
|
||||
.addTo(app);
|
||||
Reference in New Issue
Block a user