feat: update new feature temp for panel

This commit is contained in:
abearxiong 2025-03-04 01:20:55 +08:00
parent 6e96247b50
commit fd3288cb5b
45 changed files with 2559 additions and 954 deletions

4
.gitignore vendored
View File

@ -25,4 +25,6 @@ dist-ssr
*.sw? *.sw?
tsconfig.app.tsbuildinfo tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo tsconfig.node.tsbuildinfo
aidist

View File

@ -1,13 +1,23 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>Wall Note</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> <title>Workspace Wall Note</title>
<body> <script src="/system/lib/app.js"></script>
<div id="root"></div> <link rel="stylesheet" href="/aidist/wallnote.css">
<script type="module" src="/src/main.tsx"></script> <script>
</body> console.log(Object.keys(window.context));
</html> </script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!-- <script type="module" src="/aidist/router.es.js"></script> -->
<!-- <script type="module" src="/template/index.ts"></script> -->
</body>
</html>

View File

@ -1,44 +1,50 @@
{ {
"name": "wallnote", "name": "wallnote",
"private": true, "private": true,
"version": "0.0.7", "version": "0.1.0",
"type": "module", "type": "module",
"user": "apps", "user": "workspace",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:web": "cross-env WEB_DEV=true vite --mode web", "dev:web": "cross-env WEB_DEV=true vite --mode web",
"dev:prompt": "vite build -c vite.config.prompt.ts -w",
"build": "vite build", "build": "vite build",
"build:prompt": "vite build --config vite.config.prompt.ts",
"lint": "eslint .", "lint": "eslint .",
"deploy": "rsync -avz --delete dist/ light:~/apps/ai/dist",
"preview": "vite preview", "preview": "vite preview",
"prepub": "pnpm build && envision switch apps", "prepub": "pnpm build && envision switch workspace",
"pub": "envision deploy ./dist -k wallnote -v 0.0.7 -y y", "pub": "envision deploy ./dist -k wallnote -v 0.1.0 -y y",
"ev": "npm run build && npm run deploy" "ev": "npm run build && npm run deploy"
}, },
"stackblitz": {
"startCommand": "npm dev:web"
},
"author": "abearxiong <xiongxiao@xiongxiao.me>", "author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@blueprintjs/core": "^5.17.2",
"@blueprintjs/icons": "^5.19.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@kevisual/cache": "^0.0.1", "@kevisual/cache": "^0.0.1",
"@kevisual/query": "0.0.9-alpha.2", "@kevisual/query": "0.0.9-alpha.2",
"@kevisual/router": "0.0.7", "@kevisual/router": "0.0.8",
"@kevisual/system-lib": "0.0.21-beta.2",
"@kevisual/system-ui": "^0.0.3", "@kevisual/system-ui": "^0.0.3",
"@kevisual/ui": "^0.0.4-alpha-1", "@kevisual/ui": "^0.0.4-alpha-1",
"@mui/material": "^6.4.6", "@mui/material": "^6.4.6",
"@tiptap/core": "^2.11.5", "@tiptap/core": "^2.11.5",
"@tiptap/extension-code-block-lowlight": "^2.11.5", "@tiptap/extension-code-block-lowlight": "^2.11.5",
"@tiptap/extension-document": "^2.11.5",
"@tiptap/extension-highlight": "^2.11.5", "@tiptap/extension-highlight": "^2.11.5",
"@tiptap/extension-paragraph": "^2.11.5",
"@tiptap/extension-placeholder": "^2.11.5",
"@tiptap/extension-text": "^2.11.5",
"@tiptap/extension-typography": "^2.11.5", "@tiptap/extension-typography": "^2.11.5",
"@tiptap/pm": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5", "@tiptap/starter-kit": "^2.11.5",
"@tiptap/suggestion": "^2.11.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/turndown": "^5.0.5", "@types/turndown": "^5.0.5",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"antd": "^5.24.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"github-markdown-css": "^5.8.1", "github-markdown-css": "^5.8.1",
@ -53,6 +59,9 @@
"nanoid": "^5.1.2", "nanoid": "^5.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-draggable": "^4.4.6",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.7",
"react-router": "^7.2.0", "react-router": "^7.2.0",
"react-router-dom": "^7.2.0", "react-router-dom": "^7.2.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
@ -67,7 +76,8 @@
"@types/node": "^22.13.8", "@types/node": "^22.13.8",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-basic-ssl": "^1.2.0", "@types/react-resizable": "^3.0.8",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0", "eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",

1249
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,13 @@
import { Flow } from './pages/wall'; import { Flow } from './pages/wall';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Editor } from './pages/editor';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { List } from './pages/wall/pages/List';
import { Auth } from './modules/layouts/Auth'; import { Auth } from './modules/layouts/Auth';
import { basename } from './modules/basename'; // import { basename } from './modules/basename';
import 'github-markdown-css/github-markdown.css'; import 'github-markdown-css/github-markdown.css';
import { App as WallShareApp } from './pages/wall-share';
export const App = () => { export const App = () => {
return ( return (
<> <>
<BrowserRouter basename={basename}> <Flow checkLogin={false} />
<Routes>
<Route element={<Auth auth={false} />}>
<Route index path='/' element={<Flow checkLogin={false} />} />
<Route path='/editor' element={<Editor />} />
</Route>
<Route element={<Auth auth={true} />}>
<Route path='/edit/:id' element={<Flow checkLogin={true} />} />
<Route path='/list' element={<List />} />
</Route>
<Route
path='/share/*'
element={
<Auth auth={false}>
<WallShareApp />
</Auth>
}
/>
<Route path='*' element={<Navigate to='/' />} />
</Routes>
</BrowserRouter>
<ToastContainer /> <ToastContainer />
</> </>
); );

View File

@ -1,6 +1,87 @@
import { createRoot } from 'react-dom/client'; import { createRoot, Root } from 'react-dom/client';
import { App } from './App.tsx'; import { App } from './App.tsx';
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
import './index.css'; 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';
createRoot(document.getElementById('root')!).render(<App />); 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');
setTimeout(() => {
page.subscribe(
'panels',
() => {
root = createRoot(wallnoteDom!);
root.render(<ExampleApp />);
},
{ runImmediately: true },
);
}, 1000);

View File

@ -1 +1 @@
export const basename = DEV_SERVER ? '/' : BASE_NAME; export const basename = DEV_SERVER ? '' : BASE_NAME;

View File

@ -0,0 +1,33 @@
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;

View File

@ -0,0 +1,60 @@
import React from 'react';
import {
Code,
FileText,
BarChart2,
Settings,
Layers,
Database,
Server,
Terminal,
Image,
Calculator,
MessageSquare,
DivideIcon,
NotebookPen,
SquareTerminal,
} from 'lucide-react';
import { LucideIcon } from 'lucide-react';
// Map of window types to their corresponding icons
const windowTypeIcons: Record<string, LucideIcon> = {
code: Code,
document: FileText,
analytics: BarChart2,
settings: Settings,
layers: Layers,
database: Database,
server: Server,
terminal: Terminal,
image: Image,
calculator: Calculator,
welcome: MessageSquare,
notebook: NotebookPen, // 笔记本
command: SquareTerminal, // 命令行
// Add more types as needed
};
// Default colors for each window type
export const windowTypeColors: Record<string, string> = {
code: 'text-blue-600',
document: 'text-gray-600',
analytics: 'text-purple-600',
settings: 'text-gray-600',
layers: 'text-indigo-600',
database: 'text-green-600',
server: 'text-red-600',
terminal: 'text-gray-600',
image: 'text-pink-600',
calculator: 'text-yellow-600',
welcome: 'text-blue-600',
// Add more types as needed
};
// Function to get the icon component for a window type
export const getIconForWindowType = (type: string): LucideIcon => {
return windowTypeIcons[type] || MessageSquare; // Default to MessageSquare if type not found
};
export const getColorForWindowType = (type: string): string => {
return windowTypeColors[type] || 'text-gray-600'; // Default to gray if type not found
};

View File

@ -0,0 +1,420 @@
import React, { useState, useCallback, useRef, useEffect, RefObject } from 'react';
import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown } 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';
interface WindowManagerProps {
windows: WindowData[];
showTaskbar?: boolean;
onSave?: (windows: WindowData[]) => 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 [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);
// Create stable refs for each window
const windowRefs = useRef<Record<string, React.RefObject<HTMLDivElement | null>>>({});
const draggableRefs = useRef<Record<string, React.RefObject<HTMLDivElement | null>>>({});
useImperativeHandle(ref, () => ({
addWindow: (window: WindowData) => {
addWindow(window);
},
getWindows: () => {
return windows;
},
}));
useEffect(() => {
console.log('initialWindows', initialWindows);
setWindows(initialWindows);
}, [initialWindows]);
// Initialize refs for all windows
useEffect(() => {
windows.forEach((window) => {
if (!windowRefs.current[window.id]) {
windowRefs.current[window.id] = React.createRef<HTMLDivElement | null>();
}
if (!draggableRefs.current[window.id]) {
draggableRefs.current[window.id] = React.createRef<HTMLDivElement | null>();
}
});
}, [windows]);
// Initialize window positions
useEffect(() => {
const positions: Record<string, WindowPosition> = {};
windows.forEach((window) => {
positions[window.id] = {
x: 0,
y: 0,
width: 0,
height: 0,
zIndex: 1000,
...window.position,
};
});
setWindowPositions(positions);
setMaxZIndex(1000 + windows.length);
setMount(true);
}, [windows.length]);
useEffect(() => {
if (mount) {
const newWindows = windows
.map((window) => {
return {
...window,
position: windowPositions[window.id],
};
})
.sort((a, b) => a.position.zIndex - b.position.zIndex)
.map((item, index) => {
return {
...item,
position: {
...item.position,
zIndex: 1000 + index,
},
};
});
onSave?.(newWindows);
}
}, [mount, windowPositions]);
const addWindow = useCallback((window: WindowData) => {
const has = windows.find((w) => w.id === window.id);
if (has) {
setWindows((prev) => prev.map((w) => (w.id === window.id ? window : w)));
} else {
setWindows((prev) => [...prev, window]);
}
}, []);
// Handle window removal
const handleRemoveWindow = useCallback(
(windowId: string) => {
const window = windows.find((w) => w.id === windowId);
if (window?.onHidden) {
window.onHidden();
return;
}
setWindows((prev) => prev.filter((w) => w.id !== windowId));
setMinimizedWindows((prev) => prev.filter((id) => id !== windowId));
if (fullscreenWindow === windowId) {
setFullscreenWindow(null);
}
},
[fullscreenWindow],
);
// 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]);
}
if (fullscreenWindow === windowId) {
setFullscreenWindow(null);
}
},
[minimizedWindows, fullscreenWindow],
);
// Handle window fullscreen
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],
);
// Bring window to front
const bringToFront = useCallback(
(windowId: string, e?: any) => {
setActiveWindow(windowId);
setMaxZIndex((prev) => prev + 1);
setWindowPositions((prev) => ({
...prev,
[windowId]: {
...prev[windowId],
zIndex: maxZIndex + 1,
},
}));
if (e) {
e.stopPropagation();
return e.target.className.includes('window-draggable');
}
},
[maxZIndex],
);
// Handle window resize
const handleResize = useCallback((windowId: string, e: any, { size }: { size: { width: number; height: number } }) => {
// Ensure minimum dimensions are respected
const width = Math.max(MIN_WINDOW_WIDTH, size.width);
const height = Math.max(MIN_WINDOW_HEIGHT, size.height);
setWindowPositions((prev) => ({
...prev,
[windowId]: {
...prev[windowId],
width,
height,
},
}));
}, []);
// Render window controls
const renderWindowControls = useCallback(
(windowId: string) => {
const isFullscreen = fullscreenWindow === windowId;
return (
<div className='flex items-center space-x-2'>
<button
onClick={(e) => {
e.stopPropagation();
handleMinimizeWindow(windowId);
}}
className='p-1 hover:bg-gray-200 rounded'>
<SquareMinus size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleFullscreenWindow(windowId);
}}
className='p-1 hover:bg-gray-200 rounded'>
{isFullscreen ? <Minimize2 size={16} /> : <Expand size={16} />}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveWindow(windowId);
}}
className='p-1 hover:bg-red-200 rounded'>
<X size={16} />
</button>
</div>
);
},
[handleMinimizeWindow, handleFullscreenWindow, handleRemoveWindow],
);
// Render the taskbar with minimized windows
const renderTaskbar = () => {
const showWindowsList = windows.filter((window) => window.show && window.showTaskbar);
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';
// }
// });
// };
// window.addEventListener('resize', handleResize);
// handleResize(); // Initial check
// return () => {
// window.removeEventListener('resize', handleResize);
// };
// }, []);
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]'>
{showWindowsList.map((window) => {
const isMinimized = minimizedWindows.includes(window.id);
return (
<button
key={window.id}
className={classNames(
'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',
'cursor-pointer',
)}
onClick={() => handleMinimizeWindow(window.id)}>
<span className='truncate min-w-[16px]'>{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} />
<X
className='cursor-pointer x-icon'
size={16}
onClick={(e) => {
e.stopPropagation();
handleRemoveWindow(window.id);
}}
/>
</div>
</button>
);
})}
</div>
);
};
// Add this useEffect to handle window resize
// Render a fixed position window
const renderFixedWindow = (windowData: WindowData) => {
const isMinimized = minimizedWindows.includes(windowData.id);
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;
const height = isFullscreen ? window.innerHeight - 40 : position.height;
// Get or create refs for this window
if (!windowRefs.current[windowData.id]) {
windowRefs.current[windowData.id] = React.createRef<HTMLDivElement | null>();
}
if (!draggableRefs.current[windowData.id]) {
draggableRefs.current[windowData.id] = React.createRef<HTMLDivElement | null>();
}
const windowRef = windowRefs.current[windowData.id];
const draggableRef = draggableRefs.current[windowData.id];
return (
<div
key={windowData.id}
className={classNames('absolute pointer-events-auto', windowData.show && 'block', !windowData.show && 'hidden')}
style={{
left: isFullscreen ? 0 : position.x,
top: isFullscreen ? 0 : position.y,
width: width,
height: height,
zIndex: isFullscreen ? 9999 : position.zIndex,
}}
ref={windowRef}>
<div
className={classNames('window-container', isFullscreen && 'fullscreen')}
style={{
width: '100%',
height: '100%',
}}>
<Draggable
handle='.window-title-bar'
position={{ x: 0, y: 0 }}
onStart={(e) => bringToFront(windowData.id)}
onStop={(e, data) => {
if (!isFullscreen) {
// Update the window's position in the state
const newX = position.x + data.x;
const newY = position.y + data.y;
setWindowPositions((prev) => ({
...prev,
[windowData.id]: {
...prev[windowData.id],
x: newX,
y: newY,
},
}));
}
}}
nodeRef={draggableRef as RefObject<HTMLElement>}
allowAnyClick={true}
disabled={isFullscreen}>
<div className='window-draggable' ref={draggableRef}>
<ResizableBox
width={width}
height={height}
onResize={(e, data) => !isFullscreen && handleResize(windowData.id, e, data)}
// resizeHandles={isFullscreen ? [] : ['e', 's', 'se']}
resizeHandles={windowData.resizeHandles || ['e', 's', 'se']}
minConstraints={[MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT]}
draggableOpts={{ disabled: isFullscreen }}>
<div
className={classNames(
'window bg-white shadow-lg overflow-hidden border border-gray-300',
showRounded && 'rounded-lg',
isFullscreen && 'fullscreen',
activeWindow === windowData.id && 'active',
)}
style={{
width: `${width}px`,
height: `${height}px`,
}}
onClick={() => bringToFront(windowData.id)}>
<div className='window-title-bar bg-gray-100 border-b border-gray-300 px-2 py-1 flex justify-between items-center cursor-move'>
<div className='window-title font-medium flex items-center'>
{windowData.showTitle && (
<>
<Icon className='mr-2' size={20} />
{windowData.title}
</>
)}
</div>
<div className='window-controls'>{renderWindowControls(windowData.id)}</div>
</div>
<div className='window-content h-[calc(100%-32px)] overflow-auto p-4'>
<div className='h-full flex flex-col'>
<WindowContent window={windowData} />
</div>
</div>
</div>
</ResizableBox>
</div>
</Draggable>
</div>
</div>
);
};
return (
<div className='h-screen w-screen overflow-hidden' ref={containerRef}>
{windows.map((window) => renderFixedWindow(window))}
{showTaskbar && renderTaskbar()}
</div>
);
});
WindowManager.displayName = 'WindowManager';
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);
}, []);
return <div data-id={window.id} className='flex-1 overflow-auto editor-window' ref={ref}></div>;
});
export default WindowManager;

View File

@ -0,0 +1,120 @@
import { WindowData } from '../types';
export const createEditorWindow = (data: any): WindowData => {
return {
...data,
showTitle: true,
show: true,
showTaskbar: true,
showRounded: false,
};
};
const windowPositions = {
window1: {
x: 50,
y: 50,
width: 300,
height: 200,
zIndex: 1000,
},
window2: {
x: 410,
y: 50,
width: 300,
height: 200,
zIndex: 1001,
},
window3: {
x: 770,
y: 50,
width: 300,
height: 200,
zIndex: 1002,
},
window4: {
x: 1130,
y: 50,
width: 300,
height: 200,
zIndex: 1003,
},
'code-editor': {
x: 50,
y: 230,
width: 300,
height: 200,
zIndex: 1004,
},
document: {
x: 410,
y: 230,
width: 300,
height: 200,
zIndex: 1005,
},
analytics: {
x: 770,
y: 230,
width: 300,
height: 200,
zIndex: 1006,
},
settings: {
x: 1130,
y: 230,
width: 300,
height: 200,
zIndex: 1007,
},
layers: {
x: 50,
y: 410,
width: 300,
height: 200,
zIndex: 1008,
},
database: {
x: 410,
y: 410,
width: 300,
height: 200,
zIndex: 1009,
},
server: {
x: 770,
y: 410,
width: 300,
height: 200,
zIndex: 1010,
},
terminal: {
x: 1130,
y: 410,
width: 300,
height: 200,
zIndex: 1011,
},
command: {
x: 50,
y: 590,
width: 300,
height: 200,
},
};
// 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' }),
].map((window) => ({ ...window, position: windowPositions[window.id] }));

View File

@ -0,0 +1,17 @@
import { useEffect } from 'react';
export const isMac = navigator.userAgent.includes('Mac');
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') {
callback();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
};

View File

@ -0,0 +1,2 @@
import './style.css';

View File

@ -0,0 +1,119 @@
import { create } from 'zustand';
import { WindowData } from '../types';
import { MyCache } from '@kevisual/cache';
import { query } from '@/modules/query';
import { toast } from 'react-toastify';
import { getDocumentWidthAndHeight } from '../utils/document-width';
import { produce } from 'immer';
interface PanelStore {
data?: PanelData;
setData: (data: PanelData) => void;
init?: (id?: string) => Promise<any>;
id: string;
setId: (id: string) => void;
toggleAICommand: () => void;
}
interface PanelData {
/**
*
*/
windows: WindowData[];
/**
*
*/
showTaskbar: boolean;
}
export const usePanelStore = create<PanelStore>((set, get) => ({
id: '',
setId: (id: string) => set({ id }),
data: undefined,
setData: (data: PanelData) => set({ data }),
initNewEnv: async (id?: string) => {
const cache = new MyCache<PanelData>(id || 'panel');
const data = await cache.getData();
set({
data: data,
});
},
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 },
});
}
},
toggleAICommand: () => {
const { data } = get();
if (!data) {
return;
}
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 };
}
return w;
});
} else {
const { width, height } = getDocumentWidthAndHeight();
data.windows.push({
id: '__ai__',
title: 'AI Command',
type: 'commandƒ',
position: {
x: 100,
y: height - 200,
width: width - 200,
height: 200,
zIndex: 1000,
},
resizeHandles: ['se', 'sw', 'ne', 'nw', 's', 'w', 'n', 'e'],
show: true,
});
}
set({ data: { ...data, windows: data.windows } });
console.log('data', data);
},
}));

View File

@ -0,0 +1,132 @@
@import 'tailwindcss';
/* Fixed window styles */
.window {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
min-width: 300px;
min-height: 200px;
}
.window.active {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.window.fullscreen {
border-radius: 0 !important;
}
.window-container.fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 40px !important; /* Leave space for taskbar */
width: 100% !important;
height: calc(100% - 40px) !important;
z-index: 9999 !important;
}
/* Resize handles */
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
background-color: rgba(0, 0, 0, 0.2);
z-index: 10;
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: se-resize;
border-top-left-radius: 100%;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
width: 20px;
height: 20px;
cursor: sw-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
width: 20px;
height: 20px;
cursor: nw-resize;
}
.react-resizable-handle-ne {
top: 0;
right: 0;
width: 20px;
height: 20px;
cursor: ne-resize;
}
.react-resizable-handle-w {
top: 50%;
left: 0;
transform: translateY(-50%);
width: 8px;
height: 30px;
cursor: ew-resize;
}
.react-resizable-handle-e {
top: 50%;
right: 0;
transform: translateY(-50%);
width: 8px;
height: 30px;
cursor: ew-resize;
}
.react-resizable-handle-n {
top: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 8px;
cursor: ns-resize;
}
.react-resizable-handle-s {
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 8px;
cursor: ns-resize;
}
/* Taskbar styles */
.taskbar {
background-color: #1f2937;
border-top: 1px solid #374151;
}
/* Window content */
.window-content {
background-color: white;
}
.react-resizable-handle {
z-index: 1300; /* 确保手柄在其他元素之上 */
background-color: rgba(0, 0, 0, 0);
}
.react-resizable-handle-e,
.react-resizable-handle-w {
height: 100%;
}
.react-resizable-handle-s,
.react-resizable-handle-n {
width: 100%;
}

View File

@ -0,0 +1,51 @@
import { ResizeHandle } from 'react-resizable';
export interface WindowPosition {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
}
export interface WindowData {
// 窗口的唯一标识
id: string;
// 窗口的标题
title: string;
// 窗口的类型 notebook,command,code,document,image,calculator,welcome,analytics,settings,layers,database,server,terminal
type?: string;
// 是否最小化
isMinimized?: boolean;
// 是否全屏
isFullscreen?: boolean;
// 是否显示标题
showTitle?: boolean;
// 是否显示圆角
showRounded?: boolean;
// 是否显示在任务栏
showTaskbar?: boolean;
// 窗口的resize手柄
resizeHandles?: ResizeHandle[];
// 窗口的默认位置
position?: WindowPosition;
// 窗口的默认位置
defaultPosition?: WindowPosition;
// 是否显示
show?: boolean;
// 是否显示更多工具
showMoreTools?: boolean;
// 更多工具
moreTools?: MoreTool[];
// 当隐藏窗口存在,只关闭隐藏窗口,不退出程序
onHidden?: () => void;
}
export interface MoreTool {
// 工具的名称
title?: string;
description?: string;
path?: string;
key?: string;
// 工具的图标
icon?: string;
// 工具的点击事件
onClick?: () => void;
}

View File

@ -0,0 +1,14 @@
export const getDocumentWidth = () => {
return document.documentElement.clientWidth;
};
export const getDocumentHeight = () => {
return document.documentElement.clientHeight;
};
export const getDocumentWidthAndHeight = () => {
return {
width: getDocumentWidth(),
height: getDocumentHeight(),
};
};

View File

@ -0,0 +1,112 @@
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { CommandItem } from '../extensions/suggestions/commands';
interface CommandsListProps {
items: CommandItem[];
command: (props: { content: string }) => void;
}
export const CommandsList = forwardRef((props: CommandsListProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index: number) => {
const item = props.items[index];
if (item) {
props.command({ content: item.content });
}
};
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
return false;
},
}));
// Scroll to selected item when it changes
useEffect(() => {
const element = document.getElementById(`command-item-${selectedIndex}`);
if (element) {
element.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}, [selectedIndex]);
return (
<div className="bg-white rounded-md shadow-lg border border-gray-200 overflow-hidden" style={{ width: '350px', maxHeight: '80vh' }}>
<div className="p-2 bg-gray-50 border-b border-gray-200 sticky top-0 z-10">
<div className="text-sm font-medium text-gray-700">Commands ({props.items.length})</div>
<div className="text-xs text-gray-500">Type to filter commands</div>
</div>
<div className="max-h-72 overflow-y-auto">
{props.items.length ? (
props.items.map((item, index) => (
<button
id={`command-item-${index}`}
key={index}
className={`block w-full text-left px-4 py-2 text-sm transition-colors ${
index === selectedIndex ? 'bg-blue-100 border-l-4 border-blue-500' : 'border-l-4 border-transparent'
} hover:bg-gray-50`}
onClick={() => selectItem(index)}
>
<div className="font-medium flex items-center">
!{item.title}
{index === selectedIndex && (
<span className="ml-2 text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
Press Enter to select
</span>
)}
</div>
<div className="text-gray-500 text-xs">{item.description}</div>
</button>
))
) : (
<div className="px-4 py-2 text-sm text-gray-500">No results</div>
)}
</div>
<div className="bg-gray-50 px-3 py-2 text-xs text-gray-500 border-t flex justify-between items-center sticky bottom-0 z-10">
<span className="inline-flex items-center">
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm mr-1"></kbd>
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm"></kbd>
<span className="ml-1">to navigate</span>
</span>
<span className="inline-flex items-center">
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm">Enter</kbd>
<span className="ml-1">to select</span>
</span>
</div>
</div>
);
});
CommandsList.displayName = 'CommandsList';

View File

@ -0,0 +1,42 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
export class ReactRenderer {
component: any;
element: HTMLElement;
ref: React.RefObject<any>;
props: any;
editor: any;
root: any;
constructor(component: any, { props, editor }: any) {
this.component = component;
this.element = document.createElement('div');
this.ref = React.createRef();
this.props = {
...props,
ref: this.ref,
};
this.editor = editor;
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;

View File

@ -0,0 +1,11 @@
.ProseMirror p.is-empty::before {
content: attr(data-placeholder);
color: #aaa; /* Adjust the color as needed */
font-style: italic; /* Optional: make the placeholder italic */
pointer-events: none; /* Ensure the placeholder is not interactive */
height: 0; /* Ensure it doesn't affect layout */
display: block; /* Ensure it displays as a block element */
}
.tiptap .ProseMirror {
border: none;
}

View File

@ -4,6 +4,8 @@ import Highlight from '@tiptap/extension-highlight';
import Typography from '@tiptap/extension-typography'; import Typography from '@tiptap/extension-typography';
import { Markdown } from 'tiptap-markdown'; import { Markdown } from 'tiptap-markdown';
import Placeholder from '@tiptap/extension-placeholder';
import { Commands, getSuggestionItems, createSuggestionConfig } from './extensions/suggestions';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import { all, createLowlight } from 'lowlight'; import { all, createLowlight } from 'lowlight';
import 'highlight.js/styles/github.css'; import 'highlight.js/styles/github.css';
@ -13,6 +15,8 @@ import ts from 'highlight.js/lib/languages/typescript';
import html from 'highlight.js/lib/languages/xml'; import html from 'highlight.js/lib/languages/xml';
import css from 'highlight.js/lib/languages/css'; import css from 'highlight.js/lib/languages/css';
import markdown from 'highlight.js/lib/languages/markdown'; import markdown from 'highlight.js/lib/languages/markdown';
import './editor.css';
const lowlight = createLowlight(all); const lowlight = createLowlight(all);
// you can also register individual languages // you can also register individual languages
@ -31,11 +35,16 @@ export class TextEditor {
this.destroy(); this.destroy();
} }
const html = opts?.html || ''; const html = opts?.html || '';
const items = getSuggestionItems();
const suggestionConfig = createSuggestionConfig(items);
this.editor = new Editor({ this.editor = new Editor({
element: el, // 指定编辑器容器 element: el, // 指定编辑器容器
extensions: [ extensions: [
StarterKit, // 使用 StarterKit 包含基础功能 StarterKit, // 使用 StarterKit 包含基础功能
Highlight, Highlight,
Placeholder.configure({
placeholder: 'Type ! to see commands (e.g., !today, !list, !good)...',
}),
Typography, Typography,
Markdown, Markdown,
CodeBlockLowlight.extend({ CodeBlockLowlight.extend({
@ -68,6 +77,9 @@ export class TextEditor {
}).configure({ }).configure({
lowlight, lowlight,
}), }),
Commands.configure({
suggestion: suggestionConfig,
}),
], ],
content: html, // 初始化内容 content: html, // 初始化内容
}); });

View File

@ -0,0 +1,41 @@
import { Extension } from '@tiptap/core';
import Suggestion from '@tiptap/suggestion';
import { PluginKey } from '@tiptap/pm/state';
export const CommandsPluginKey = new PluginKey('commands');
export interface CommandItem {
title: string;
description: string;
content: string;
}
export const Commands = Extension.create({
name: 'commands',
addOptions() {
return {
suggestion: {
char: '!',
command: ({ editor, range, props }: any) => {
console.log('sdfsd')
editor
.chain()
.focus()
.deleteRange(range)
.insertContent(props.content)
.run();
},
},
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
];
},
});

View File

@ -0,0 +1,3 @@
export * from './commands';
export * from './suggestionConfig';
export * from './suggestionItems';

View File

@ -0,0 +1,121 @@
import { CommandItem } from './commands';
import { CommandsList } from '../../components/CommandsList';
import ReactRenderer from '../../components/ReactRenderer';
export const createSuggestionConfig = (items: CommandItem[]) => {
return {
items: ({ query }: { query: string }) => {
return items.filter(item => item.title.toLowerCase().startsWith(query.toLowerCase()));
},
render: () => {
let component: ReactRenderer | null = null;
let popup: HTMLElement | null = null;
const calculatePosition = (view: any, from: number) => {
const coords = view.coordsAtPos(from);
const editorRect = view.dom.getBoundingClientRect();
const popupRect = popup?.getBoundingClientRect();
if (!popup || !popupRect) return { left: coords.left, top: coords.bottom + 10 };
// Default position below the cursor
let left = coords.left;
let top = coords.bottom + 10;
// Check if we're near the bottom of the viewport
const viewportHeight = window.innerHeight;
const bottomSpace = viewportHeight - coords.bottom;
const popupHeight = popupRect.height;
// If there's not enough space below, position above
if (bottomSpace < popupHeight + 10 && coords.top > popupHeight + 10) {
top = coords.top - popupHeight - 10;
}
// Check if we're near the right edge of the viewport
const viewportWidth = window.innerWidth;
const rightSpace = viewportWidth - coords.left;
const popupWidth = popupRect.width;
// If there's not enough space to the right, align right edge
if (rightSpace < popupWidth) {
left = Math.max(10, viewportWidth - popupWidth - 10);
}
// Ensure popup stays within editor bounds horizontally if possible
if (left < editorRect.left) {
left = editorRect.left;
}
return { left, top };
};
return {
onStart: (props: any) => {
component = new ReactRenderer(CommandsList, {
props,
editor: props.editor,
});
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';
document.body.appendChild(popup);
popup.appendChild(component.element);
// Initial position
const { view } = props.editor;
const { from } = props.range;
// Set initial position to get popup dimensions
popup.style.left = '0px';
popup.style.top = '0px';
// Calculate proper position after the popup is rendered
setTimeout(() => {
if (!popup) return;
const { left, top } = calculatePosition(view, from);
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
}, 0);
},
onUpdate: (props: any) => {
if (!component) return;
component.updateProps(props);
if (!popup) return;
// Update position
const { view } = props.editor;
const { from } = props.range;
const { left, top } = calculatePosition(view, from);
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
},
onKeyDown: (props: any) => {
if (props.event.key === 'Escape') {
if (popup) popup.remove();
if (component) component.destroy();
return true;
}
if (component && component.ref && component.ref.current) {
return component.ref.current.onKeyDown(props);
}
return false;
},
onExit: () => {
if (popup) popup.remove();
if (component) component.destroy();
component = null;
popup = null;
},
};
},
};
};

View File

@ -0,0 +1,203 @@
import { CommandItem } from './commands';
export const getSuggestionItems = (): CommandItem[] => {
// Basic commands
const basicCommands = [
{
title: 'today',
description: 'Insert today\'s date',
content: new Date().toLocaleDateString(),
},
{
title: 'now',
description: 'Insert current time',
content: new Date().toLocaleTimeString(),
},
{
title: 'datetime',
description: 'Insert current date and time',
content: new Date().toLocaleString(),
},
{
title: 'list',
description: 'Insert a bullet list',
content: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
},
{
title: 'numbered',
description: 'Insert a numbered list',
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
},
{
title: 'good',
description: 'Insert a positive message',
content: 'Great job! Keep up the good work! 👍',
},
{
title: 'meeting',
description: 'Insert meeting template',
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
},
{
title: 'signature',
description: 'Insert your signature',
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
},
];
// Text formatting commands
const formattingCommands = [
{
title: 'h1',
description: 'Insert heading 1',
content: '<h1>Heading 1</h1>',
},
{
title: 'h2',
description: 'Insert heading 2',
content: '<h2>Heading 2</h2>',
},
{
title: 'h3',
description: 'Insert heading 3',
content: '<h3>Heading 3</h3>',
},
{
title: 'quote',
description: 'Insert blockquote',
content: '<blockquote>This is a quote</blockquote>',
},
{
title: 'code',
description: 'Insert code block',
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
},
{
title: 'bold',
description: 'Insert bold text',
content: '<strong>Bold text</strong>',
},
{
title: 'italic',
description: 'Insert italic text',
content: '<em>Italic text</em>',
},
{
title: 'underline',
description: 'Insert underlined text',
content: '<u>Underlined text</u>',
},
{
title: 'strike',
description: 'Insert strikethrough text',
content: '<s>Strikethrough text</s>',
},
{
title: 'highlight',
description: 'Insert highlighted text',
content: '<mark>Highlighted text</mark>',
},
];
// Template commands
const templateCommands = [
{
title: 'email',
description: 'Insert email template',
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
},
{
title: 'letter',
description: 'Insert formal letter template',
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
},
{
title: 'report',
description: 'Insert report template',
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
},
{
title: 'proposal',
description: 'Insert proposal template',
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
},
{
title: 'invoice',
description: 'Insert invoice template',
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
},
];
// Task management commands
const taskCommands = [
{
title: 'todo',
description: 'Insert todo list',
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
},
{
title: 'checklist',
description: 'Insert checklist',
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
},
{
title: 'progress',
description: 'Insert progress tracker',
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
},
{
title: 'timeline',
description: 'Insert project timeline',
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
},
{
title: 'goals',
description: 'Insert goals list',
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
},
];
// Table commands
const tableCommands = [
{
title: 'table2x2',
description: 'Insert 2x2 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
},
{
title: 'table3x3',
description: 'Insert 3x3 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
},
{
title: 'schedule',
description: 'Insert schedule table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
},
{
title: 'comparison',
description: 'Insert comparison table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
},
];
// Additional commands to reach 100 total
const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => {
const index = i + 1;
return {
title: `command${index}`,
description: `Example command ${index}`,
content: `<p>This is example command ${index}</p>`,
};
});
// Combine all command categories
return [
...basicCommands,
...formattingCommands,
...templateCommands,
...taskCommands,
...tableCommands,
...additionalCommands,
];
};

View File

@ -0,0 +1,203 @@
import { CommandItem } from './commands';
export const getSuggestionItems = (): CommandItem[] => {
// Basic commands
const basicCommands = [
{
title: 'today',
description: 'Insert today\'s date',
content: new Date().toLocaleDateString(),
},
{
title: 'now',
description: 'Insert current time',
content: new Date().toLocaleTimeString(),
},
{
title: 'datetime',
description: 'Insert current date and time',
content: new Date().toLocaleString(),
},
{
title: 'list',
description: 'Insert a bullet list',
content: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
},
{
title: 'numbered',
description: 'Insert a numbered list',
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
},
{
title: 'good',
description: 'Insert a positive message',
content: 'Great job! Keep up the good work! 👍',
},
{
title: 'meeting',
description: 'Insert meeting template',
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
},
{
title: 'signature',
description: 'Insert your signature',
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
},
];
// Text formatting commands
const formattingCommands = [
{
title: 'h1',
description: 'Insert heading 1',
content: '<h1>Heading 1</h1>',
},
{
title: 'h2',
description: 'Insert heading 2',
content: '<h2>Heading 2</h2>',
},
{
title: 'h3',
description: 'Insert heading 3',
content: '<h3>Heading 3</h3>',
},
{
title: 'quote',
description: 'Insert blockquote',
content: '<blockquote>This is a quote</blockquote>',
},
{
title: 'code',
description: 'Insert code block',
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
},
{
title: 'bold',
description: 'Insert bold text',
content: '<strong>Bold text</strong>',
},
{
title: 'italic',
description: 'Insert italic text',
content: '<em>Italic text</em>',
},
{
title: 'underline',
description: 'Insert underlined text',
content: '<u>Underlined text</u>',
},
{
title: 'strike',
description: 'Insert strikethrough text',
content: '<s>Strikethrough text</s>',
},
{
title: 'highlight',
description: 'Insert highlighted text',
content: '<mark>Highlighted text</mark>',
},
];
// Template commands
const templateCommands = [
{
title: 'email',
description: 'Insert email template',
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
},
{
title: 'letter',
description: 'Insert formal letter template',
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
},
{
title: 'report',
description: 'Insert report template',
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
},
{
title: 'proposal',
description: 'Insert proposal template',
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
},
{
title: 'invoice',
description: 'Insert invoice template',
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
},
];
// Task management commands
const taskCommands = [
{
title: 'todo',
description: 'Insert todo list',
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
},
{
title: 'checklist',
description: 'Insert checklist',
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
},
{
title: 'progress',
description: 'Insert progress tracker',
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
},
{
title: 'timeline',
description: 'Insert project timeline',
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
},
{
title: 'goals',
description: 'Insert goals list',
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
},
];
// Table commands
const tableCommands = [
{
title: 'table2x2',
description: 'Insert 2x2 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
},
{
title: 'table3x3',
description: 'Insert 3x3 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
},
{
title: 'schedule',
description: 'Insert schedule table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
},
{
title: 'comparison',
description: 'Insert comparison table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
},
];
// Additional commands to reach 100 total
const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => {
const index = i + 1;
return {
title: `command${index}`,
description: `Example command ${index}`,
content: `<p>This is example command ${index}</p>`,
};
});
// Combine all command categories
return [
...basicCommands,
...formattingCommands,
...templateCommands,
...taskCommands,
...tableCommands,
...additionalCommands,
];
};

View File

@ -1,2 +1,13 @@
// <a href="https://kevisual.xiongxiao.me/workspace/wallnote/">wallnote v1版工作区/workspace/wallnote)</a> // <a href="https://kevisual.xiongxiao.me/workspace/wallnote/">wallnote v1版工作区/workspace/wallnote)</a>
export const DOCS_NODE = [{"id":"e15owpuh9cv3fgwx5zymtc","position":{"x":-1613.6078090729259,"y":-726.9366215444888},"data":{"html":"<h1>Wallnote 基本使用介绍 v0.0.7</h1><p></p><p>可拖拽的随笔记功能。</p><ul class=\"tight\" data-tight=\"true\"><li><p>纯网页界面,数据存储在浏览器(不登陆情况下,只有单个页面)</p></li><li><p>这个墙随便拖动</p></li><li><p>双击空格添加一条记录并打开编辑esc关闭</p></li><li><p>富文本编辑器md语法</p></li><li><p>点击节点聚焦后delete删除</p></li><li><p>右键空白处粘贴</p><ul class=\"tight\" data-tight=\"true\"><li><p>html的内容编辑会丢失样式</p></li><li><p>图片的内容(粘贴后不能编辑)</p></li><li><p>文本内容</p></li><li><p>复制的节点信息</p></li></ul></li><li><p>边框可拖动大小</p></li></ul><h3>注意</h3><ul class=\"tight\" data-tight=\"true\"><li><p>点击节点聚焦后,如果有滚动条,节点内容才能滚动</p></li><li><p>图片复制只能是二进制文件夹的图片复制后无效。比如snipaste 贴图复制Can To Do。</p></li></ul><p></p><h2>登录后功能</h2><ul class=\"tight\" data-tight=\"true\"><li><p>保存而不是临时编辑</p></li></ul><h2>新版</h2><p></p><p>![ai-workspace](https://kevisual.xiongxiao.me/workspace/wallnote/)</p><p></p><h2>TODO</h2><ul class=\"tight\" data-tight=\"true\"><li><p>do do do</p></li><li><p>ai ++++</p></li></ul>","width":1113,"height":444},"type":"wallnote"},{"id":"kb0vbz4ffi1x6aw8clo0ho","position":{"x":-1613.4790358693674,"y":-256.0352475384902},"data":{"width":356,"height":50,"html":"<a href=\"https://kevisual.xiongxiao.me/workspace/wallnote/\">wallnote v1版工作区/workspace/wallnote)</a>"},"type":"wallnote"}] export const DOCS_NODE = [
{
id: 'e15owpuh9cv3fgwx5zymtc',
position: { x: -1498.479327713264, y: -734.7581188564604 },
data: {
html: '<h1>Wallnote 基本使用介绍 v0.1.0</h1><p></p><p>可拖拽的随笔记功能。</p><ul class="tight" data-tight="true"><li><p>这个墙随便拖动</p></li><li><p>双击空的地方添加一条文本记录并打开编辑esc关闭</p></li><li><p>富文本编辑器md语法</p></li><li><p>点击节点聚焦后delete删除</p></li><li><p>右键空白处粘贴</p><ul class="tight" data-tight="true"><li><p>html的内容编辑会丢失样式</p></li><li><p>图片的内容(粘贴后不能编辑)</p></li><li><p>文本内容</p></li><li><p>复制的节点信息</p></li></ul></li><li><p>边框可拖动大小</p></li></ul><h3>注意</h3><ul class="tight" data-tight="true"><li><p>点击节点聚焦后,如果有滚动条,节点内容才能滚动</p></li><li><p>图片复制只能是二进制文件夹的图片复制后无效读不到文件。比如snipaste 贴图复制。</p></li></ul>',
width: 1113,
height: 444,
},
type: 'wallnote',
},
];

View File

@ -25,7 +25,7 @@ import { message } from '@/modules/message';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { BlankNoteText } from './constants'; import { BlankNoteText } from './constants';
import { Toolbar } from './modules/toolbar/Toolbar'; import { Toolbar } from './modules/toolbar/Toolbar';
import { useNavigate, useParams } from 'react-router-dom'; // import { useNavigate, useParams } from 'react-router-dom';
import { SaveModal } from './modules/FormDialog'; import { SaveModal } from './modules/FormDialog';
import { useTabNode } from './hooks/tab-node'; import { useTabNode } from './hooks/tab-node';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
@ -210,8 +210,9 @@ export function FlowContent() {
); );
} }
export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => { export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
const { id } = useParams(); // const { id } = useParams();
const navigate = useNavigate(); const id = '';
// const navigate = useNavigate();
const wallStore = useWallStore( const wallStore = useWallStore(
useShallow((state) => { useShallow((state) => {
return { return {
@ -236,7 +237,7 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
<Button <Button
variant='contained' variant='contained'
onClick={() => { onClick={() => {
navigate('/'); // navigate('/');
wallStore.clearId(); wallStore.clearId();
}}> }}>

View File

@ -195,7 +195,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, onClose }) => {
backgroundColor: 'white', backgroundColor: 'white',
border: '1px solid #ccc', border: '1px solid #ccc',
width: 200, width: 200,
zIndex: 1000, zIndex: 100,
}} }}
onMouseLeave={onClose}> onMouseLeave={onClose}>
{menuList.map((item) => ( {menuList.map((item) => (

View File

@ -5,7 +5,7 @@ import { getNodeData, useWallStore } from '../store/wall';
import { useReactFlow, useStore } from '@xyflow/react'; import { useReactFlow, useStore } from '@xyflow/react';
import { useUserWallStore, Wall } from '../store/user-wall'; import { useUserWallStore, Wall } from '../store/user-wall';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { useNavigate } from 'react-router-dom'; // import { useNavigate } from 'react-router-dom';
import { WallData } from './CustomNode'; import { WallData } from './CustomNode';
function FormDialog({ open, handleClose, handleSubmit, initialData }) { function FormDialog({ open, handleClose, handleSubmit, initialData }) {
@ -93,7 +93,7 @@ export const SaveModal = () => {
const userWallStore = useUserWallStore(useShallow((state) => state)); const userWallStore = useUserWallStore(useShallow((state) => state));
const { showFormDialog, setShowFormDialog, formDialogData, setFormDialogData } = wallStore; const { showFormDialog, setShowFormDialog, formDialogData, setFormDialogData } = wallStore;
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
const navigate = useNavigate(); // const navigate = useNavigate();
const { id } = wallStore; const { id } = wallStore;
const onSubmit = async (values) => { const onSubmit = async (values) => {
const nodes = reactFlowInstance.getNodes(); const nodes = reactFlowInstance.getNodes();
@ -122,7 +122,7 @@ export const SaveModal = () => {
message.info('redirect to edit page'); message.info('redirect to edit page');
wallStore.clear(); wallStore.clear();
setTimeout(() => { setTimeout(() => {
navigate(`/edit/${data.id}`); // navigate(`/edit/${data.id}`);
}, 2000); }, 2000);
} else { } else {
// 编辑 // 编辑

View File

@ -7,7 +7,6 @@ import { useUserWallStore } from '../../store/user-wall';
import { redirectToLogin } from '@/modules/require-to-login'; import { redirectToLogin } from '@/modules/require-to-login';
import { useStore } from '@xyflow/react'; import { useStore } from '@xyflow/react';
import { message } from '@/modules/message'; import { message } from '@/modules/message';
import { useNavigate } from 'react-router-dom';
import { ClickAwayListener } from '@mui/material'; import { ClickAwayListener } from '@mui/material';
export const ToolbarItem = ({ export const ToolbarItem = ({
children, children,
@ -68,7 +67,6 @@ export const ToolbarContent = ({ open }) => {
const userWallStore = useUserWallStore(useShallow((state) => state)); const userWallStore = useUserWallStore(useShallow((state) => state));
const store = useStore((state) => state); const store = useStore((state) => state);
const hasLogin = !!userWallStore.user; const hasLogin = !!userWallStore.user;
const navigate = useNavigate();
const menuList: MenuItem[] = [ const menuList: MenuItem[] = [
{ {
@ -149,7 +147,7 @@ export const ToolbarContent = ({ open }) => {
key: 'myWall', key: 'myWall',
icon: <BrickWall />, icon: <BrickWall />,
onClick: () => { onClick: () => {
navigate('/list'); //
}, },
}); });
} }
@ -170,7 +168,7 @@ export const ToolbarContent = ({ open }) => {
onClick: async () => { onClick: async () => {
const res = await userWallStore.deleteWall(wallStore.id!); const res = await userWallStore.deleteWall(wallStore.id!);
if (res.code === 200) { if (res.code === 200) {
navigate('/'); // navigate('/');
} }
}, },
}); });
@ -212,7 +210,7 @@ export const ToolbarContent = ({ open }) => {
key: 'add', key: 'add',
icon: <Plus />, icon: <Plus />,
onClick: () => { onClick: () => {
navigate(`/`); // navigate(`/`);
wallStore.clearQueryWall(); wallStore.clearQueryWall();
}, },
}); });
@ -226,7 +224,7 @@ export const ToolbarContent = ({ open }) => {
if (res.code === 200) { if (res.code === 200) {
message.success('删除成功,返回首页'); message.success('删除成功,返回首页');
wallStore.clearQueryWall(); wallStore.clearQueryWall();
navigate('/'); // navigate('/');
} }
}, },
}); });

View File

@ -3,8 +3,12 @@ import { MyCache } from '@kevisual/cache';
const cache = new MyCache('cacheWall'); const cache = new MyCache('cacheWall');
export async function getWallData() { export async function getWallData() {
const data = await cache.getData(); try {
return data; const data = await cache.getData();
return data;
} catch (e) {
cache.del();
}
} }
export async function setWallData(data: any) { export async function setWallData(data: any) {

View File

@ -0,0 +1,5 @@
import { useEffect } from 'react';
export const AiApp = () => {
return <div>AiApp</div>;
};

28
template/ai-app/main.tsx Normal file
View File

@ -0,0 +1,28 @@
import { createRoot, Root } from 'react-dom/client';
import { AiApp } from './AiApp';
import { app, initAIAppRootOrCreate, useContextKey } from '../app';
import { Editor } from '@/pages/editor/index';
import { ExampleApp } from '@/modules/panels/Example';
initAIAppRootOrCreate();
app
.route({
path: 'ai',
key: 'render',
description: '渲染AI应用',
run: async (ctx) => {
const root = initAIAppRootOrCreate();
console.log('ai render');
console.log('ai render', root);
if (!root) {
return;
}
const aiRoot = createRoot(root!);
// aiRoot.render(<Editor />);
// aiRoot.render(<AiApp />);
aiRoot.render(<ExampleApp />);
useContextKey('aiRoot', () => aiRoot, true);
ctx.body = 'aiRoot';
},
})
.addTo(app);

View File

@ -1,3 +1,39 @@
import { QueryAI } from '@kevisual/query/query-ai'; import { QueryAI } from '@kevisual/query/query-ai';
import { MyCache } from '@kevisual/cache'; import { MyCache } from '@kevisual/cache';
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
import { QueryRouterServer } from '@kevisual/system-lib/dist/router-browser';
import { QueryClient } from '@kevisual/system-lib/dist/query-browser';
import { Page } from '@kevisual/system-lib/dist/web-page';
import { BaseLoad } from '@kevisual/system-lib/dist/load';
import { Message } from '@kevisual/system-ui/dist/message';
export { useContextKey };
export const message = useContextKey<Message>('message', () => {
return new Message();
});
export const load = useContextKey<BaseLoad>('load', () => {
return new BaseLoad();
});
export const app = useContextKey<QueryRouterServer>('app');
export const page = useContextKey<Page>('page');
export const query = useContextKey<QueryClient>('query', () => {
return new QueryClient({
io: true,
});
});
export const workCache = useContextKey<MyCache>('workCache', () => {
return new MyCache('work');
});
export const queryAI = useContextKey<QueryAI>('queryAI', () => {
return new QueryAI();
});
export const initAIAppRootOrCreate = () => {
const root = document.getElementById('ai-root');
if (!root) {
const root = document.createElement('div');
root.id = 'ai-root';
document.body.appendChild(root);
}
return root;
};

39
template/index.ts Normal file
View File

@ -0,0 +1,39 @@
import { app, page, load } from './app';
import './ai-app/main';
import './tailwind.css';
import './workspace/entry';
page.addPage('/', 'workspace');
const runLoad = () => {
load.load(
() => {
// @TODO 这里需要优化,不能每次都去调用
page.subscribe(
'workspace',
async () => {
await app.call({
path: 'workspace',
key: 'enter',
});
setTimeout(() => {
app.call({
path: 'ai',
key: 'render',
});
}, 1000);
},
{ runImmediately: true },
);
},
{
key: 'workspaceRoute',
isReRun: true,
checkSuccess: () => {
return page.pageModule.has('workspace');
},
},
);
};
// runLoad()

32
template/tailwind.css Normal file
View File

@ -0,0 +1,32 @@
@import 'tailwindcss';
#ai-root {
width: 100%;
height: 100%;
background-color: #f0f0f0;
position: fixed;
top: 0;
left: 0;
z-index: 900;
}
/* # 背景设置为透明 */
#ai-root {
/* @apply bg-transparent */
@apply border-none box-border border-2 border-gray-300 rounded-md;
@apply pointer-events-none;
}
#ai-root {
.node-editor {
@apply pointer-events-auto;
@apply absolute bottom-0 left-0 w-full h-[20vh] border border-gray-300 shadow-md;
.tiptap {
@apply mx-0 h-full overflow-y-auto;
}
}
#app {
@apply w-full h-full overflow-hidden;
}
}

6
template/user/route.ts Normal file
View File

@ -0,0 +1,6 @@
import { app } from '../app';
app.route({
path: 'user',
key: 'login',
});

View File

@ -0,0 +1,50 @@
import { message } from '../../app';
import { query } from '../../app';
import { createStore } from 'zustand/vanilla';
type User = {
id: string;
username?: string;
nickname?: string;
avatar: string;
};
interface UserWallStore {
user?: User;
setUser: (user: User) => void;
/**
*
* @param openOnNoLogin
* @returns
*/
queryMe: (openOnNoLogin?: boolean) => Promise<void>;
logout: () => void;
isLogin: () => boolean;
}
export const useUserWallStore = createStore<UserWallStore>((set, get) => ({
user: undefined,
setUser: (user: User) => set({ user }),
queryMe: async (openOnNoLogin = true) => {
const res = await query.post(
{
path: 'user',
key: 'me',
},
{
afterResponse: !openOnNoLogin ? async (res) => res : undefined,
},
);
console.log('queryMe', res);
if (res.code === 200) {
set({ user: res.data });
}
},
logout: () => {
set({ user: undefined });
localStorage.removeItem('token');
},
isLogin: () => {
return !!get().user;
},
}));

View File

@ -0,0 +1,25 @@
import { app } from '../app';
app
.route({
path: 'wallnote',
key: 'list',
validator: {
pagiantion: {
type: 'object',
properties: {
page: {
type: 'number',
},
},
},
},
run: async (ctx) => {
const { pagiantion } = ctx.validator.pagiantion;
const { page } = pagiantion;
const { data } = await ctx.query.get('/wallnote/list', {
page,
});
},
})
.addTo(app);

View File

@ -0,0 +1,29 @@
import { app, page } from '../app';
import { message } from '@kevisual/system-ui/dist/message';
let isRender = false;
app
.route({
path: 'workspace',
key: 'enter',
run: async (ctx) => {
// 第一次进入页面获取用户信息如果没有登陆则去登陆TODO
// 只根据id来判断工作区。
const url = new URL(location.href);
const isWorksapce = url.pathname.startsWith('/workspace');
if (!isWorksapce) {
message.error('请先进入工作区');
return;
}
console.log('workspace enter');
if (!isRender) {
app.call({
path: 'wallnote',
key: 'render',
});
isRender = true;
}
ctx.body = '';
},
})
.addTo(app);

View File

@ -38,6 +38,7 @@
}, },
"include": [ "include": [
"src", "src",
"typings.d.ts" "typings.d.ts",
"template"
] ]
} }

View File

@ -18,5 +18,5 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts", "vite.config.prompt.ts"]
} }

35
vite.config.prompt.ts Normal file
View File

@ -0,0 +1,35 @@
import { defineConfig } from 'vite';
import path from 'path';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import pkgs from './package.json' with { type: 'json' };
const version = pkgs.version || '0.0.1';
const isDev = process.env.NODE_ENV === 'development';
const basename = isDev ? '/' : '/workspace/wallnote';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(),tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@template': path.resolve(__dirname, './template'),
},
},
base: basename,
define: {
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
VERSION: JSON.stringify(version),
BASE_NAME: JSON.stringify(basename),
},
build: {
target: 'esnext',
lib: {
entry: 'template/index.ts', // 设置入口文件
formats: ['es'],
fileName: (format) => `router.${format}.js` // 打包后的文件名
},
outDir: 'aidist',
},
});

View File

@ -9,9 +9,9 @@ const version = pkgs.version || '0.0.1';
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
const basename = isDev ? '/' : '/apps/wallnote'; const basename = isDev ? '/' : '/workspace/wallnote';
const plugins = [] const plugins = []
const isWeb = false; const isWeb = true;
const isKevisual = true; const isKevisual = true;
if(isWeb) { if(isWeb) {
@ -27,13 +27,18 @@ if(isKevisual) {
}, },
'/api/router': { '/api/router': {
// target: 'ws://localhost:3000', // target: 'ws://localhost:3000',
target: 'https://kevisual.xiongxiao.me', // target: 'https://kevisual.xiongxiao.me',
target: 'wss://kevisual.xiongxiao.me',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
rewriteWsOrigin: true, rewriteWsOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'), rewrite: (path) => path.replace(/^\/api/, '/api'),
}, },
'/root/center': { '/system/lib': {
target: 'https://kevisual.xiongxiao.me/',
changeOrigin: true,
},
'/user/login': {
target: 'https://kevisual.xiongxiao.me', target: 'https://kevisual.xiongxiao.me',
changeOrigin: true, changeOrigin: true,
}, },
@ -55,10 +60,21 @@ export default defineConfig({
}, },
build: { build: {
target: 'esnext', target: 'esnext',
// rollupOptions: {
// input: {
// index: './index.html',
// },
// },
},
optimizeDeps: {
exclude: ['aidist'],
}, },
server: { server: {
port: 6004, port: 6004,
host: '0.0.0.0', host: '0.0.0.0',
watch: {
ignored: ['aidist'],
},
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:3000',