generated from template/vite-react-template
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
3eaf262ee8 | |||
4d0e945a92 | |||
c1247eba32 | |||
cbef0943de | |||
4a04a432ca | |||
fd3288cb5b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,3 +26,5 @@ dist-ssr
|
|||||||
|
|
||||||
tsconfig.app.tsbuildinfo
|
tsconfig.app.tsbuildinfo
|
||||||
tsconfig.node.tsbuildinfo
|
tsconfig.node.tsbuildinfo
|
||||||
|
|
||||||
|
aidist
|
1
.npmrc
1
.npmrc
@@ -1,3 +1,2 @@
|
|||||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||||
@abearxiong:registry=https://npm.pkg.github.com
|
|
||||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
22
index.html
22
index.html
@@ -1,13 +1,23 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Wall Note</title>
|
<title>Workspace Wall Note</title>
|
||||||
</head>
|
<script src="/system/lib/app.js"></script>
|
||||||
<body>
|
<link rel="stylesheet" href="/aidist/wallnote.css">
|
||||||
|
<script>
|
||||||
|
console.log(Object.keys(window.context));
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<!-- <script type="module" src="/src/main.tsx"></script> -->
|
||||||
</body>
|
<!-- <script type="module" src="/aidist/router.es.js"></script> -->
|
||||||
|
<script type="module" src="/template/index.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
82
package.json
82
package.json
@@ -1,46 +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": "^6.0.0",
|
||||||
"@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.15",
|
||||||
"@kevisual/router": "0.0.7",
|
"@kevisual/router": "0.0.10",
|
||||||
|
"@kevisual/system-lib": "0.0.22",
|
||||||
"@kevisual/system-ui": "^0.0.3",
|
"@kevisual/system-ui": "^0.0.3",
|
||||||
"@kevisual/ui": "^0.0.4-alpha-1",
|
"@mui/material": "^7.0.1",
|
||||||
"@mui/material": "^6.4.6",
|
"@tiptap/core": "^2.11.7",
|
||||||
"@tiptap/core": "^2.11.5",
|
"@tiptap/extension-code-block-lowlight": "^2.11.7",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.11.5",
|
"@tiptap/extension-document": "^2.11.7",
|
||||||
"@tiptap/extension-highlight": "^2.11.5",
|
"@tiptap/extension-highlight": "^2.11.7",
|
||||||
"@tiptap/extension-typography": "^2.11.5",
|
"@tiptap/extension-paragraph": "^2.11.7",
|
||||||
"@tiptap/starter-kit": "^2.11.5",
|
"@tiptap/extension-placeholder": "^2.11.7",
|
||||||
|
"@tiptap/extension-text": "^2.11.7",
|
||||||
|
"@tiptap/extension-typography": "^2.11.7",
|
||||||
|
"@tiptap/pm": "^2.11.7",
|
||||||
|
"@tiptap/starter-kit": "^2.11.7",
|
||||||
|
"@tiptap/suggestion": "^2.11.7",
|
||||||
"@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.5.4",
|
||||||
"antd": "^5.24.2",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
@@ -48,36 +52,40 @@
|
|||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.487.0",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
"nanoid": "^5.1.2",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router": "^7.2.0",
|
"react-draggable": "^4.4.6",
|
||||||
"react-router-dom": "^7.2.0",
|
"react-resizable": "^3.0.5",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"react-router": "^7.4.1",
|
||||||
|
"react-router-dom": "^7.4.1",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tiptap-markdown": "^0.8.10",
|
"tiptap-markdown": "^0.8.10",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@kevisual/types": "^0.0.6",
|
"@kevisual/types": "^0.0.6",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.1.1",
|
||||||
"@types/node": "^22.13.8",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.1.0",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.1.1",
|
||||||
"@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.23.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tailwindcss": "^4.0.9",
|
"tailwindcss": "^4.1.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.25.0",
|
"typescript-eslint": "^8.29.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
1736
pnpm-lock.yaml
generated
1736
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
31
src/App.tsx
31
src/App.tsx
@@ -1,38 +1,15 @@
|
|||||||
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 = () => {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const id = url.searchParams.get('id') || undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BrowserRouter basename={basename}>
|
<Flow checkLogin={false} id={id} />
|
||||||
<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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -34,7 +34,8 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body,.tiptap {
|
.markdown-body,
|
||||||
|
.tiptap {
|
||||||
ul,
|
ul,
|
||||||
li {
|
li {
|
||||||
list-style: unset;
|
list-style: unset;
|
||||||
@@ -47,4 +48,10 @@ iframe {
|
|||||||
border: unset;
|
border: unset;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
/* will-change: transform; */
|
||||||
|
/* pointer-events: none; */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
|
18
src/main.tsx
18
src/main.tsx
@@ -1,6 +1,16 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { page, app } from './routes';
|
||||||
import { App } from './App.tsx';
|
|
||||||
|
|
||||||
import './index.css';
|
page.addPage('/', 'wallnote');
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />);
|
setTimeout(() => {
|
||||||
|
page.subscribe(
|
||||||
|
'wallnote',
|
||||||
|
() => {
|
||||||
|
app.call({
|
||||||
|
path: 'wallnote',
|
||||||
|
key: 'render',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ 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');
|
@@ -1 +1 @@
|
|||||||
export const basename = DEV_SERVER ? '/' : BASE_NAME;
|
export const basename = DEV_SERVER ? '' : BASE_NAME;
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
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;
|
60
src/modules/panels/components/WindowIcons.tsx
Normal file
60
src/modules/panels/components/WindowIcons.tsx
Normal 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
|
||||||
|
};
|
468
src/modules/panels/components/WindowManager.tsx
Normal file
468
src/modules/panels/components/WindowManager.tsx
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
import React, { useState, useCallback, useRef, useEffect, RefObject, useMemo } from 'react';
|
||||||
|
import { Maximize2, Minimize2, Minimize, Expand, X, SquareMinus, Maximize, ChevronDown, CommandIcon, LogOut } from 'lucide-react';
|
||||||
|
import { WindowData, WindowPosition } from '../types';
|
||||||
|
import classNames from 'clsx';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
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, onCommand }: WindowManagerProps, ref) => {
|
||||||
|
const [windows, setWindows] = useState<WindowData[]>(initialWindows);
|
||||||
|
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>>>({});
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
addWindow: (window: WindowData) => {
|
||||||
|
addWindow(window);
|
||||||
|
},
|
||||||
|
getWindows: () => {
|
||||||
|
return windows;
|
||||||
|
},
|
||||||
|
setWindows: (windows: WindowData[]) => {
|
||||||
|
console.log('setWindows in manager', windows);
|
||||||
|
setWindows(windows);
|
||||||
|
setUpdate((prev) => prev + 1);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
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, update]);
|
||||||
|
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);
|
||||||
|
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));
|
||||||
|
setWindows((prev) =>
|
||||||
|
prev.map((w) => {
|
||||||
|
if (w.id === windowId) {
|
||||||
|
return { ...w, isMinimized: false };
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (fullscreenWindow === windowId) {
|
||||||
|
setFullscreenWindow(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fullscreenWindow],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle window minimize
|
||||||
|
const handleMinimizeWindow = useCallback(
|
||||||
|
(windowId: string) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[, fullscreenWindow],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle window fullscreen
|
||||||
|
const handleFullscreenWindow = useCallback((windowId: string) => {
|
||||||
|
setFullscreenWindow((prev) => (prev === windowId ? null : windowId));
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
(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);
|
||||||
|
// };
|
||||||
|
// }, []);
|
||||||
|
const showLogout = useMemo(() => {
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className=' pointer-events-auto fixed w-full overflow-x-auto bottom-0 left-0 right-0 bg-gray-200 text-white p-2 flex space-x-2 z-[9000] h-[40px]'>
|
||||||
|
{showLogout && (
|
||||||
|
<div
|
||||||
|
className='flex items-center space-x-2 cursor-pointer bg-blue-600 rounded-md p-1'
|
||||||
|
onClick={() => {
|
||||||
|
context?.app?.call?.({
|
||||||
|
path: 'user',
|
||||||
|
key: 'logout',
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<LogOut size={16} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className='flex items-center space-x-2 cursor-pointer bg-blue-600 rounded-md p-1'
|
||||||
|
onClick={() => {
|
||||||
|
onCommand?.();
|
||||||
|
}}>
|
||||||
|
<CommandIcon size={16} />
|
||||||
|
</div>
|
||||||
|
{showWindowsList.map((window) => {
|
||||||
|
const isMinimized = window.isMinimized;
|
||||||
|
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',
|
||||||
|
'bar-button',
|
||||||
|
'cursor-pointer',
|
||||||
|
)}
|
||||||
|
onClick={() => handleMinimizeWindow(window.id)}>
|
||||||
|
<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} />
|
||||||
|
<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 = 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;
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
const zIndex = isFullscreen ? 9999 : windowData.id == '__ai__' ? 3000 : position.zIndex;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={windowData.id}
|
||||||
|
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: zIndex,
|
||||||
|
}}
|
||||||
|
ref={windowRef}>
|
||||||
|
<div
|
||||||
|
className={classNames('window-container', isFullscreen && 'fullscreen', showTaskbar && 'hidden-taskbar', windowData.show && !isMinimized && 'block')}
|
||||||
|
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'>
|
||||||
|
<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(() => {
|
||||||
|
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>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default WindowManager;
|
120
src/modules/panels/demo/DemoWindows.tsx
Normal file
120
src/modules/panels/demo/DemoWindows.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { WindowData } from '../types';
|
||||||
|
|
||||||
|
export const createDemoEditorWindow = (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[] = [
|
||||||
|
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] }));
|
17
src/modules/panels/hooks/use-listen-b.ts
Normal file
17
src/modules/panels/hooks/use-listen-b.ts
Normal 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 ((event.metaKey || event.ctrlKey) && event.key === 'b') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
3
src/modules/panels/index.tsx
Normal file
3
src/modules/panels/index.tsx
Normal file
@@ -0,0 +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();
|
||||||
|
});
|
115
src/modules/panels/render/manager/manager.ts
Normal file
115
src/modules/panels/render/manager/manager.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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;
|
||||||
|
console.log('base render', render, render?.command);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('render error', res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: 'nodeRender',
|
||||||
|
payload: {
|
||||||
|
pageId: pageId,
|
||||||
|
id: nodeData.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...windowData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
196
src/modules/panels/store/index.ts
Normal file
196
src/modules/panels/store/index.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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';
|
||||||
|
import { createEditorWindow } from './create/create-editor-window';
|
||||||
|
import { createDemoEditorWindow } from '../demo/DemoWindows';
|
||||||
|
|
||||||
|
interface PanelStore {
|
||||||
|
data?: PanelData;
|
||||||
|
setData: (data: PanelData) => void;
|
||||||
|
init?: (id?: string) => Promise<any>;
|
||||||
|
id: string;
|
||||||
|
setId: (id: string) => void;
|
||||||
|
toggleAICommand: (windows: WindowData[]) => WindowData[];
|
||||||
|
saveWindows: (windows: WindowData[]) => void;
|
||||||
|
setEditorWindow: (windowData: WindowData) => void;
|
||||||
|
closeEditorWindow: (id: string) => 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 },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
set({
|
||||||
|
data: {
|
||||||
|
// windows: [e.windowData],
|
||||||
|
windows: [],
|
||||||
|
showTaskbar: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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__') {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { width, height } = getDocumentWidthAndHeight();
|
||||||
|
data.windows.push({
|
||||||
|
id: '__ai__',
|
||||||
|
title: '🤖 AI Command',
|
||||||
|
type: 'command',
|
||||||
|
showTitle: true,
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: height - 200 - 40,
|
||||||
|
width: width - 200,
|
||||||
|
height: 200,
|
||||||
|
zIndex: 1000,
|
||||||
|
},
|
||||||
|
resizeHandles: ['se', 'sw', 'ne', 'nw', 's', 'w', 'n', 'e'],
|
||||||
|
show: true,
|
||||||
|
render: {
|
||||||
|
command: {
|
||||||
|
path: 'editor',
|
||||||
|
key: 'render',
|
||||||
|
payload: {
|
||||||
|
id: '__ai__',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// set({ data: { ...data, windows: data.windows } });
|
||||||
|
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);
|
136
src/modules/panels/style.css
Normal file
136
src/modules/panels/style.css
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
@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);
|
||||||
|
z-index: 9900 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen.hidden-taskbar {
|
||||||
|
height: calc(100% - 0px) !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%;
|
||||||
|
}
|
59
src/modules/panels/types.ts
Normal file
59
src/modules/panels/types.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ResizeHandle } from 'react-resizable';
|
||||||
|
export interface WindowPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
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;
|
||||||
|
// 窗口的标题
|
||||||
|
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?: WindowCommandData[];
|
||||||
|
// 工具列表
|
||||||
|
commandList?: WindowCommandData[];
|
||||||
|
// 渲染
|
||||||
|
render?: {
|
||||||
|
command: WindowCommand;
|
||||||
|
props?: any;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
}
|
14
src/modules/panels/utils/document-width.ts
Normal file
14
src/modules/panels/utils/document-width.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
};
|
112
src/modules/tiptap/components/CommandsList.tsx
Normal file
112
src/modules/tiptap/components/CommandsList.tsx
Normal 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';
|
42
src/modules/tiptap/components/ReactRenderer.tsx
Normal file
42
src/modules/tiptap/components/ReactRenderer.tsx
Normal 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;
|
11
src/modules/tiptap/editor.css
Normal file
11
src/modules/tiptap/editor.css
Normal 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;
|
||||||
|
}
|
@@ -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, CommandItem } 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
|
||||||
@@ -24,18 +28,29 @@ lowlight.register('markdown', markdown);
|
|||||||
|
|
||||||
export class TextEditor {
|
export class TextEditor {
|
||||||
private editor?: Editor;
|
private editor?: Editor;
|
||||||
|
private opts?: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void };
|
||||||
|
private element?: HTMLElement;
|
||||||
|
private isInitialSetup: boolean = true;
|
||||||
|
|
||||||
constructor() {}
|
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) {
|
if (this.editor) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
this.opts = opts;
|
||||||
|
this.element = el;
|
||||||
const html = opts?.html || '';
|
const html = opts?.html || '';
|
||||||
|
const items = opts?.items || getSuggestionItems();
|
||||||
|
const suggestionConfig = createSuggestionConfig(items);
|
||||||
|
this.isInitialSetup = true;
|
||||||
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 @test )...',
|
||||||
|
}),
|
||||||
Typography,
|
Typography,
|
||||||
Markdown,
|
Markdown,
|
||||||
CodeBlockLowlight.extend({
|
CodeBlockLowlight.extend({
|
||||||
@@ -68,12 +83,41 @@ export class TextEditor {
|
|||||||
}).configure({
|
}).configure({
|
||||||
lowlight,
|
lowlight,
|
||||||
}),
|
}),
|
||||||
|
Commands.configure({
|
||||||
|
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) {
|
updateSugestionConfig(items: CommandItem[]) {
|
||||||
this.editor?.commands.setContent(html);
|
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() {
|
getHtml() {
|
||||||
return this.editor?.getHTML();
|
return this.editor?.getHTML();
|
||||||
@@ -81,12 +125,6 @@ export class TextEditor {
|
|||||||
getContent() {
|
getContent() {
|
||||||
return this.editor?.getText();
|
return this.editor?.getText();
|
||||||
}
|
}
|
||||||
onContentChange(callback: (html: string) => void) {
|
|
||||||
this.editor?.off('update'); // 移除之前的监听
|
|
||||||
this.editor?.on('update', () => {
|
|
||||||
callback(this.editor?.getHTML() || '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
foucus() {
|
foucus() {
|
||||||
this.editor?.view?.focus?.();
|
this.editor?.view?.focus?.();
|
||||||
}
|
}
|
||||||
|
40
src/modules/tiptap/extensions/suggestions/commands.ts
Normal file
40
src/modules/tiptap/extensions/suggestions/commands.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.insertContent(props.content)
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
Suggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
3
src/modules/tiptap/extensions/suggestions/index.ts
Normal file
3
src/modules/tiptap/extensions/suggestions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './commands';
|
||||||
|
export * from './suggestionConfig';
|
||||||
|
export * from './suggestionItems';
|
121
src/modules/tiptap/extensions/suggestions/suggestionConfig.ts
Normal file
121
src/modules/tiptap/extensions/suggestions/suggestionConfig.ts
Normal 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 = '9999';
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
203
src/modules/tiptap/extensions/suggestions/suggestionItems.ts
Normal file
203
src/modules/tiptap/extensions/suggestions/suggestionItems.ts
Normal 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,
|
||||||
|
];
|
||||||
|
};
|
203
src/modules/tiptap/extensions/suggestions/suggestions.ts
Normal file
203
src/modules/tiptap/extensions/suggestions/suggestions.ts
Normal 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,
|
||||||
|
];
|
||||||
|
};
|
40
src/pages/demo-login/index.css
Normal file
40
src/pages/demo-login/index.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
.demo-login-prompt {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-login-link {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-login-link a {
|
||||||
|
color: #4a90e2;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-login-link a:hover {
|
||||||
|
color: #357abd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-accounts {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-account {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 0.3rem;
|
||||||
|
color: #333;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
27
src/pages/demo-login/index.tsx
Normal file
27
src/pages/demo-login/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from '@mui/material';
|
||||||
|
import './index.css';
|
||||||
|
export const AppendDemo = () => {
|
||||||
|
return (
|
||||||
|
<div className='demo-login-prompt'>
|
||||||
|
<p className='demo-login-link'>
|
||||||
|
<a href='/user/login'>请先登录</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DemoLogin = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center h-screen gap-4'>
|
||||||
|
<AppendDemo />
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = `/user/login/?redirect=${window.location.href}&username=demo&password=xiong1015`;
|
||||||
|
}}>
|
||||||
|
试用登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
92
src/pages/editor/NodeTextEditor.tsx
Normal file
92
src/pages/editor/NodeTextEditor.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Node Edit Editor
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
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,62 @@
|
|||||||
import { TextEditor } from '@/modules/tiptap/editor';
|
import { TextEditor } from '@/modules/tiptap/editor';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export const useListenCtrlEnter = (callback: () => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey && event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
export const Editor = ({ className, value, onChange, id }: EditorProps) => {
|
export const AiEditor = ({ className, value, onChange, id }: EditorProps) => {
|
||||||
const textEditorRef = useRef<TextEditor | null>(null);
|
const textEditorRef = useRef<TextEditor | null>(null);
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
const [mount, setMount] = useState(false);
|
const [mount, setMount] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editor = new TextEditor();
|
const editor = new TextEditor();
|
||||||
textEditorRef.current = editor;
|
textEditorRef.current = editor;
|
||||||
editor.createEditor(editorRef.current!, { html: value });
|
editor.createEditor(editorRef.current!, {
|
||||||
editor.onContentChange((content) => {
|
html: value,
|
||||||
onChange?.(content);
|
onUpdateHtml: (html) => {
|
||||||
|
onChange?.(html);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setMount(true);
|
setMount(true);
|
||||||
return () => {
|
return () => {
|
||||||
editor.destroy();
|
editor.destroy();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
useListenCtrlEnter(() => {
|
||||||
|
context?.app.call({
|
||||||
|
path: 'command',
|
||||||
|
key: 'handle',
|
||||||
|
payload: {
|
||||||
|
html: textEditorRef.current?.getHtml() || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textEditorRef.current && id && mount) {
|
if (textEditorRef.current && id && mount) {
|
||||||
textEditorRef.current.setContent(value || '');
|
textEditorRef.current.setContent(value || '');
|
||||||
}
|
}
|
||||||
}, [id, mount]);
|
}, [id, mount]);
|
||||||
return <div ref={editorRef} className={clsx('w-full h-full node-editor', className)}></div>;
|
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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
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');
|
||||||
|
|
||||||
|
|
@@ -22,3 +22,14 @@ export function SplitButtons({ closeToast }: ToastContentProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toast(SplitButtons, {
|
||||||
|
// closeButton: false,
|
||||||
|
// className: 'p-0 w-[400px] border border-purple-600/40',
|
||||||
|
// ariaLabel: 'Email received',
|
||||||
|
// onClose: (reason) => {
|
||||||
|
// if (reason === 'success') {
|
||||||
|
// set({ open: true, selectedNode: data, hasEdited: false });
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
@@ -1 +1,10 @@
|
|||||||
export const BlankNoteText = '<i>double click to edit</i>';
|
export const BlankNoteText = '<i>double click to edit</i>';
|
||||||
|
|
||||||
|
// https://www.reactbits.dev/text-animations/circular-text
|
||||||
|
export const CircularText =
|
||||||
|
'<iframe src="//player.bilibili.com/player.html?isOutside=true&aid=114057235467353&bvid=BV1ALPseyE7n&cid=28549582521&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>';
|
||||||
|
|
||||||
|
export const CircularText2 =
|
||||||
|
'<iframe src="https://kevisual.xiongxiao.me/apps/questions/" sandbox="allow-scripts allow-same-origin" width="100%" height="100%" ></iframe>';
|
||||||
|
|
||||||
|
export const CircularText3 = `<iframe srcdoc="<p>Your content here</p>"></iframe>`;
|
||||||
|
@@ -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></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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@@ -4,10 +4,10 @@ import { useWallStore } from '../store/wall';
|
|||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
export const useTabNode = () => {
|
export const useTabNode = () => {
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const open = useWallStore(useShallow((state) => state.open));
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) return;
|
|
||||||
const listener = (event: any) => {
|
const listener = (event: any) => {
|
||||||
|
const selected = reactFlowInstance.getNodes().find((node) => node.selected);
|
||||||
|
if (!selected) return;
|
||||||
if (event.key === 'Tab') {
|
if (event.key === 'Tab') {
|
||||||
const nodes = reactFlowInstance.getNodes();
|
const nodes = reactFlowInstance.getNodes();
|
||||||
const selectedNode = nodes.find((node) => node.selected);
|
const selectedNode = nodes.find((node) => node.selected);
|
||||||
@@ -56,9 +56,19 @@ export const useTabNode = () => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const rightClickListener = (event: any) => {
|
||||||
|
const selected = reactFlowInstance.getNodes().find((node) => node.selected);
|
||||||
|
if (!selected) return;
|
||||||
|
if (event.button === 2) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
window.addEventListener('keydown', listener);
|
window.addEventListener('keydown', listener);
|
||||||
|
window.addEventListener('contextmenu', rightClickListener);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', listener);
|
window.removeEventListener('keydown', listener);
|
||||||
|
window.removeEventListener('contextmenu', rightClickListener);
|
||||||
};
|
};
|
||||||
}, [reactFlowInstance, open]);
|
}, [reactFlowInstance]);
|
||||||
};
|
};
|
||||||
|
@@ -20,12 +20,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useCheckDoubleClick } from './hooks/check-double-click';
|
import { useCheckDoubleClick } from './hooks/check-double-click';
|
||||||
import { randomId } from './utils/random';
|
import { randomId } from './utils/random';
|
||||||
import { CustomNodeType } from './modules/CustomNode';
|
import { CustomNodeType } from './modules/CustomNode';
|
||||||
import Drawer from './modules/Drawer';
|
|
||||||
import { message } from '@/modules/message';
|
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';
|
||||||
@@ -33,6 +32,9 @@ import { useListenPaster } from './hooks/listen-copy';
|
|||||||
import { ContextMenu } from './modules/ContextMenu';
|
import { ContextMenu } from './modules/ContextMenu';
|
||||||
import { useSelect } from './hooks/use-select';
|
import { useSelect } from './hooks/use-select';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { AppendDemo, DemoLogin } from '../demo-login';
|
||||||
|
import { List } from './pages/List';
|
||||||
|
|
||||||
type NodeData = {
|
type NodeData = {
|
||||||
id: string;
|
id: string;
|
||||||
position: XYPosition;
|
position: XYPosition;
|
||||||
@@ -47,6 +49,7 @@ export function FlowContent() {
|
|||||||
return {
|
return {
|
||||||
nodes: state.nodes,
|
nodes: state.nodes,
|
||||||
saveNodes: state.saveNodes,
|
saveNodes: state.saveNodes,
|
||||||
|
saveDataNode: state.saveDataNode,
|
||||||
checkAndOpen: state.checkAndOpen,
|
checkAndOpen: state.checkAndOpen,
|
||||||
mouseSelect: state.mouseSelect, // 鼠标模式,不能拖动
|
mouseSelect: state.mouseSelect, // 鼠标模式,不能拖动
|
||||||
setMouseSelect: state.setMouseSelect,
|
setMouseSelect: state.setMouseSelect,
|
||||||
@@ -69,7 +72,7 @@ export function FlowContent() {
|
|||||||
wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id));
|
wallStore.saveNodes(reactFlowInstance.getNodes().filter((item) => item.id !== change.id));
|
||||||
}
|
}
|
||||||
if (change.type === 'position' && change.dragging === false) {
|
if (change.type === 'position' && change.dragging === false) {
|
||||||
getNewNodes(false);
|
getNewNodes(false, changes);
|
||||||
}
|
}
|
||||||
onNodesChange(changes);
|
onNodesChange(changes);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -96,9 +99,15 @@ export function FlowContent() {
|
|||||||
const onNodeDoubleClick = (event, node) => {
|
const onNodeDoubleClick = (event, node) => {
|
||||||
wallStore.checkAndOpen(true, node);
|
wallStore.checkAndOpen(true, node);
|
||||||
};
|
};
|
||||||
const getNewNodes = (showMessage = true) => {
|
const getNewNodes = (showMessage = true, changes?: NodeChange[]) => {
|
||||||
const nodes = reactFlowInstance.getNodes();
|
const nodes = reactFlowInstance.getNodes();
|
||||||
wallStore.saveNodes(nodes, { showMessage: showMessage });
|
// wallStore.saveNodes(nodes, { showMessage: showMessage });
|
||||||
|
// console.log('change', changes);
|
||||||
|
const operateNodes = nodes.filter((node) => {
|
||||||
|
return changes?.some((change) => change.type === 'position' && change.id === node.id);
|
||||||
|
});
|
||||||
|
console.log('operateNodes', operateNodes);
|
||||||
|
wallStore.saveDataNode(operateNodes);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mount) {
|
if (mount) {
|
||||||
@@ -125,7 +134,8 @@ export function FlowContent() {
|
|||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wallStore.checkAndOpen(true, newNode);
|
wallStore.checkAndOpen(true, newNode);
|
||||||
getNewNodes();
|
// getNewNodes();
|
||||||
|
wallStore.saveDataNode([newNode]);
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
const hasFoucedNode = useMemo(() => {
|
const hasFoucedNode = useMemo(() => {
|
||||||
@@ -187,11 +197,10 @@ export function FlowContent() {
|
|||||||
<Toolbar />
|
<Toolbar />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<Drawer />
|
|
||||||
<SaveModal />
|
<SaveModal />
|
||||||
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
{contextMenu && <ContextMenu x={contextMenu.x} y={contextMenu.y} onClose={handleCloseContextMenu} />}
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>{' '}
|
</ReactFlow>
|
||||||
{isSelecting && selectionBox && (
|
{isSelecting && selectionBox && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -209,9 +218,14 @@ export function FlowContent() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
export const Flow = ({ id }: { checkLogin?: boolean; id?: string }) => {
|
||||||
const { id } = useParams();
|
const token = localStorage.getItem('token');
|
||||||
const navigate = useNavigate();
|
if (!token) {
|
||||||
|
return <DemoLogin />;
|
||||||
|
}
|
||||||
|
if (!id) {
|
||||||
|
return <List />;
|
||||||
|
}
|
||||||
const wallStore = useWallStore(
|
const wallStore = useWallStore(
|
||||||
useShallow((state) => {
|
useShallow((state) => {
|
||||||
return {
|
return {
|
||||||
@@ -223,24 +237,17 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
wallStore.init(id);
|
wallStore.init(id);
|
||||||
console.log('checkLogin', checkLogin, id);
|
}
|
||||||
}, [id, checkLogin]);
|
}, [id]);
|
||||||
|
|
||||||
if (!wallStore.loaded) {
|
if (!wallStore.loaded) {
|
||||||
return <div>loading...</div>;
|
return <div>loading...</div>;
|
||||||
} else if (wallStore.loaded === 'error') {
|
} else if (wallStore.loaded === 'error') {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center h-screen gap-4'>
|
<div className='flex flex-col items-center justify-center h-screen gap-4'>
|
||||||
<div className='text-2xl font-bold'>获取失败,请稍后刷新重试,或者转到首页</div>
|
<div className='text-2xl font-bold'>获取失败,请稍后刷新重试</div>
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
onClick={() => {
|
|
||||||
navigate('/');
|
|
||||||
wallStore.clearId();
|
|
||||||
}}>
|
|
||||||
转到首页
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,13 +257,3 @@ export const Flow = ({ checkLogin = true }: { checkLogin?: boolean }) => {
|
|||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const FlowStatus = () => {
|
|
||||||
const { nodes } = useWallStore();
|
|
||||||
const reactFlow = useReactFlow();
|
|
||||||
const flowStore = useStore((state) => state);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>节点数量: {nodes.length}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@@ -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) => (
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
:root {
|
||||||
|
--xy-resize-background-color: #000;
|
||||||
|
}
|
||||||
@layer components {
|
@layer components {
|
||||||
.node-editor {
|
.node-editor {
|
||||||
@apply w-full h-full bg-white;
|
@apply w-full h-full bg-white;
|
||||||
@@ -40,7 +42,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tiptap {
|
.tiptap {
|
||||||
margin: 0.5rem 1rem;
|
/* margin: 0.5rem 1rem; */
|
||||||
|
margin: 0;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
.tiptap h2 {
|
.tiptap h2 {
|
||||||
/* margin-top: 3.5rem; */
|
/* margin-top: 3.5rem; */
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap h1 {
|
.tiptap h1 {
|
||||||
@@ -130,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tiptap mark {
|
.tiptap mark {
|
||||||
background-color: #FAF594;
|
background-color: #faf594;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
|
@@ -5,19 +5,17 @@ import { useWallStore } from '../store/wall';
|
|||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { message } from '@/modules/message';
|
import { message } from '@/modules/message';
|
||||||
|
import { app } from '../app';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import { Edit } from 'lucide-react';
|
import { Edit } from 'lucide-react';
|
||||||
export type WallData<T = Record<string, any>> = {
|
export type WallData<T = Record<string, any>> = {
|
||||||
html: string;
|
html: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
updatedAt?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} & T;
|
} & T;
|
||||||
const ShowContent = (props: { data: WallData; selected: boolean }) => {
|
const ShowContent = (props: { data: WallData; id: string; selected: boolean }) => {
|
||||||
const html = props.data.html;
|
|
||||||
const selected = props.selected;
|
|
||||||
const showRef = useRef<HTMLDivElement>(null);
|
|
||||||
if (!html) return <div className='w-full h-full flex items-center justify-center '>空</div>;
|
|
||||||
const [highlightHtml, setHighlightHtml] = useState('');
|
const [highlightHtml, setHighlightHtml] = useState('');
|
||||||
const highlight = async (html: string) => {
|
const highlight = async (html: string) => {
|
||||||
const _html = html.replace(/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g, (match, p1, p2) => {
|
const _html = html.replace(/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g, (match, p1, p2) => {
|
||||||
@@ -26,19 +24,23 @@ const ShowContent = (props: { data: WallData; selected: boolean }) => {
|
|||||||
return _html;
|
return _html;
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
highlight(html).then((res) => {
|
highlight(props.data.html).then((res) => {
|
||||||
setHighlightHtml(res);
|
setHighlightHtml(res);
|
||||||
});
|
});
|
||||||
}, [html]);
|
}, [props.data.html]);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = props.id;
|
||||||
|
const container = document.querySelector('.id' + id);
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = highlightHtml;
|
||||||
|
}
|
||||||
|
}, [highlightHtml, props.data.updatedAt]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={showRef}
|
className={clsx('p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white markdown-body', {}, 'id' + props.id)}
|
||||||
className='p-2 w-full h-full overflow-y-auto scrollbar tiptap bg-white markdown-body'
|
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: selected ? 'auto' : 'none',
|
pointerEvents: props.selected ? 'auto' : 'none',
|
||||||
}}
|
}}></div>
|
||||||
dangerouslySetInnerHTML={{ __html: highlightHtml }}></div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
const wallStore = useWallStore(
|
const wallStore = useWallStore(
|
||||||
useShallow((state) => {
|
useShallow((state) => {
|
||||||
return {
|
return {
|
||||||
setSelectedNode: state.setSelectedNode,
|
id: state.id,
|
||||||
saveNodes: state.saveNodes,
|
saveNodes: state.saveNodes,
|
||||||
checkAndOpen: state.checkAndOpen,
|
checkAndOpen: state.checkAndOpen,
|
||||||
};
|
};
|
||||||
@@ -83,52 +85,50 @@ 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 width = data.width || 100;
|
||||||
const height = data.height || 100;
|
const height = data.height || 100;
|
||||||
const style: React.CSSProperties = {};
|
|
||||||
style.width = width;
|
|
||||||
style.height = height;
|
|
||||||
const showOpen = () => {
|
const showOpen = () => {
|
||||||
const node = store.getNode(props.id);
|
const node = store.getNode(props.id);
|
||||||
console.log('node eidt', node);
|
console.log('node eidt', node);
|
||||||
if (node) {
|
app.call({
|
||||||
const dataType: string = (node?.data?.dataType as string) || '';
|
path: 'panels',
|
||||||
if (dataType && dataType?.startsWith('image')) {
|
key: 'add-editor-window',
|
||||||
message.error('不支持编辑图片');
|
payload: {
|
||||||
return;
|
data: {
|
||||||
} else if (dataType) {
|
pageId: wallStore.id || 'local-browser',
|
||||||
message.error('不支持编辑');
|
type: 'wallnote',
|
||||||
return;
|
nodeData: node,
|
||||||
}
|
},
|
||||||
wallStore.checkAndOpen(true, node);
|
},
|
||||||
} else {
|
});
|
||||||
message.error('节点不存在');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const handleSize = Math.max(10, 10 / zoom);
|
const handleSize = Math.max(8, 8 / zoom);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div
|
||||||
|
className={clsx('absolute -top-2 left-0 bg-gray-300 z-10 w-full h-2 custom-dragger cursor-move', {
|
||||||
|
'opacity-0': !props.selected,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
width: `calc(100% + ${handleSize}px)`,
|
||||||
|
transform: `translateX(-${handleSize / 2}px)`,
|
||||||
|
}}></div>
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
showOpen();
|
showOpen();
|
||||||
// e.stopPropagation();
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
className={clsx('w-full h-full border relative border-gray-300 min-w-[100px] min-h-[50px] tiptap-preview')}
|
className={clsx('w-full h-full border relative border-gray-300 min-w-[100px] min-h-[50px] tiptap-preview', {
|
||||||
style={style}>
|
'pointer-events-none': !props.selected,
|
||||||
<ShowContent data={data} selected={props.selected} />
|
'pointer-events-auto': props.selected,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}}>
|
||||||
|
<ShowContent data={data} id={props.id} selected={props.selected} />
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx('absolute top-0 right-0 cursor-pointer', props.selected ? 'opacity-100' : 'opacity-0')}>
|
<div className={clsx('absolute top-0 right-0 cursor-pointer', props.selected ? 'opacity-100' : 'opacity-0')}>
|
||||||
<button
|
<button
|
||||||
@@ -144,6 +144,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
minHeight={50}
|
minHeight={50}
|
||||||
onResizeStart={() => {}}
|
onResizeStart={() => {}}
|
||||||
isVisible={props.selected}
|
isVisible={props.selected}
|
||||||
|
color='#d1d5dc'
|
||||||
onResizeEnd={(e) => {
|
onResizeEnd={(e) => {
|
||||||
const parent = contentRef.current?.parentElement;
|
const parent = contentRef.current?.parentElement;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
@@ -159,6 +160,7 @@ export const CustomNode = (props: { id: string; data: WallData; selected: boolea
|
|||||||
? {
|
? {
|
||||||
width: handleSize,
|
width: handleSize,
|
||||||
height: handleSize,
|
height: handleSize,
|
||||||
|
border: 'unset',
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
@@ -1,122 +0,0 @@
|
|||||||
import { useWallStore } from '../store/wall'; // 确保导入正确的路径
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { X } from 'lucide-react'; // 导入 Close 图标
|
|
||||||
import { Editor } from '@/pages/editor';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useStore, useStoreApi } from '@xyflow/react';
|
|
||||||
import { BlankNoteText } from '../constants';
|
|
||||||
import { message } from '@/modules/message';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
import { isMac } from '../utils/is-mac';
|
|
||||||
const Drawer = () => {
|
|
||||||
const { open, setOpen, selectedNode, setSelectedNode, editValue, setEditValue, hasEdited, setHasEdited } = useWallStore(
|
|
||||||
useShallow((state) => ({
|
|
||||||
open: state.open,
|
|
||||||
setOpen: state.setOpen,
|
|
||||||
selectedNode: state.selectedNode,
|
|
||||||
setSelectedNode: state.setSelectedNode,
|
|
||||||
editValue: state.editValue,
|
|
||||||
setEditValue: state.setEditValue,
|
|
||||||
hasEdited: state.hasEdited,
|
|
||||||
setHasEdited: state.setHasEdited,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
const store = useStore((state) => state);
|
|
||||||
const storeApi = useStoreApi();
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && selectedNode) {
|
|
||||||
setEditValue(selectedNode?.data.html, true);
|
|
||||||
}
|
|
||||||
}, [open, selectedNode]);
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
return () => {
|
|
||||||
setOpen(false);
|
|
||||||
setHasEdited(false);
|
|
||||||
setSelectedNode(null);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
const listener = async (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemKey = e.metaKey || e.ctrlKey;
|
|
||||||
|
|
||||||
// mac command+s windows ctrl+s
|
|
||||||
if (systemKey && e.key === 's') {
|
|
||||||
onSave();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('keydown', listener);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', listener);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open && mounted) {
|
|
||||||
if (hasEdited) {
|
|
||||||
onSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [open, hasEdited, mounted]);
|
|
||||||
const onSave = () => {
|
|
||||||
const wallStore = useWallStore.getState();
|
|
||||||
const selectedNode = wallStore.selectedNode;
|
|
||||||
const _editValue = wallStore.editValue;
|
|
||||||
if (selectedNode && _editValue) {
|
|
||||||
selectedNode.data.html = _editValue;
|
|
||||||
const newNodes = storeApi.getState().nodes.map((node) => (node.id === selectedNode.id ? selectedNode : node));
|
|
||||||
storeApi.setState({ nodes: newNodes });
|
|
||||||
if (wallStore.id) {
|
|
||||||
message.success('保存到服务器成功', {
|
|
||||||
closeOnClick: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.success('保存到本地成功', {
|
|
||||||
closeOnClick: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
wallStore.saveNodes(newNodes, { showMessage: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let html = selectedNode?.data?.html || '';
|
|
||||||
if (html === BlankNoteText) {
|
|
||||||
html = '';
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'transition-all duration-300 bg-white flex flex-col gap-2 h-full w-full overflow-hidden fixed right-0 top-0 z-10',
|
|
||||||
open ? 'open' : 'hidden',
|
|
||||||
'w-[800px] xs:w-[100%] sm:w-[100%] md:w-[600px] lg:w-[600px] xl:w-[600px] 2xl:w-[800px]', // 默认宽度,根据屏幕大小适配,小屏幕全屏幕
|
|
||||||
)}>
|
|
||||||
<div className='flex justify-between items-center h-10'>
|
|
||||||
<button onClick={() => setOpen(false)}>
|
|
||||||
<X className='w-6 h-6 cursor-pointer ml-2' />
|
|
||||||
</button>
|
|
||||||
{selectedNode && (
|
|
||||||
<div>
|
|
||||||
<button className='bg-blue-500 text-white px-4 py-1 rounded-md mr-4 cursor-pointer' onClick={onSave}>
|
|
||||||
保存
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='pr-4 mx-4 mb-4 rounded-md pb-4 box-border scrollbar border border-gray-300 '
|
|
||||||
style={{
|
|
||||||
height: 'calc(100vh - 2.5rem)',
|
|
||||||
overflowY: 'auto',
|
|
||||||
}}>
|
|
||||||
{selectedNode && open && <Editor className='drawer-editor' value={html} onChange={setEditValue} id={selectedNode.id} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Drawer;
|
|
@@ -1,11 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button, Chip } from '@mui/material';
|
import { Dialog, DialogTitle, DialogContent, TextField, DialogActions, Button, Chip } from '@mui/material';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { getNodeData, useWallStore } from '../store/wall';
|
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,9 +93,14 @@ 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 onSubmit = useCallback(
|
||||||
|
async (values) => {
|
||||||
const { id } = wallStore;
|
const { id } = wallStore;
|
||||||
const onSubmit = async (values) => {
|
if (!id) {
|
||||||
|
message.error('请先保存到账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const nodes = reactFlowInstance.getNodes();
|
const nodes = reactFlowInstance.getNodes();
|
||||||
const data = {
|
const data = {
|
||||||
nodes: getNodeData(nodes),
|
nodes: getNodeData(nodes),
|
||||||
@@ -122,7 +127,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 {
|
||||||
// 编辑
|
// 编辑
|
||||||
@@ -131,7 +136,9 @@ export const SaveModal = () => {
|
|||||||
} else {
|
} else {
|
||||||
message.error('保存失败');
|
message.error('保存失败');
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[reactFlowInstance, wallStore.id],
|
||||||
|
);
|
||||||
if (!showFormDialog) {
|
if (!showFormDialog) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -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[] = [
|
||||||
{
|
{
|
||||||
@@ -132,37 +130,7 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '清空',
|
|
||||||
key: 'clear',
|
|
||||||
icon: <Trash />,
|
|
||||||
onClick: async () => {
|
|
||||||
await wallStore.clear();
|
|
||||||
message.success('清空成功');
|
|
||||||
store.setNodes([]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
if (hasLogin) {
|
|
||||||
menuList.unshift({
|
|
||||||
label: '我的笔记',
|
|
||||||
key: 'myWall',
|
|
||||||
icon: <BrickWall />,
|
|
||||||
onClick: () => {
|
|
||||||
navigate('/list');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!hasLogin) {
|
|
||||||
menuList.push({
|
|
||||||
label: '登录',
|
|
||||||
key: 'login',
|
|
||||||
icon: <User />,
|
|
||||||
onClick: () => {
|
|
||||||
redirectToLogin();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (wallStore.id) {
|
|
||||||
menuList.push({
|
menuList.push({
|
||||||
label: '删除',
|
label: '删除',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
@@ -170,28 +138,11 @@ 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('/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!wallStore.id) {
|
|
||||||
menuList.push({
|
|
||||||
label: '保存到账号',
|
|
||||||
key: 'saveToAccount',
|
|
||||||
icon: <Save />,
|
|
||||||
onClick: () => {
|
|
||||||
wallStore.setShowFormDialog(true);
|
|
||||||
wallStore.setFormDialogData({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
tags: [],
|
|
||||||
summary: '',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
menuList.push({
|
menuList.push({
|
||||||
label: '编辑信息',
|
label: '编辑信息',
|
||||||
key: 'saveToAccount',
|
key: 'saveToAccount',
|
||||||
@@ -207,31 +158,6 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
menuList.push({
|
|
||||||
label: '新增',
|
|
||||||
key: 'add',
|
|
||||||
icon: <Plus />,
|
|
||||||
onClick: () => {
|
|
||||||
navigate(`/`);
|
|
||||||
wallStore.clearQueryWall();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
menuList.push({
|
|
||||||
label: '删除',
|
|
||||||
key: 'delete',
|
|
||||||
icon: <Trash />,
|
|
||||||
className: 'text-red-500',
|
|
||||||
onClick: async () => {
|
|
||||||
const res = await userWallStore.deleteWall(wallStore.id!);
|
|
||||||
if (res.code === 200) {
|
|
||||||
message.success('删除成功,返回首页');
|
|
||||||
wallStore.clearQueryWall();
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
menuList.push({
|
menuList.push({
|
||||||
label: '退出 ',
|
label: '退出 ',
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
@@ -240,7 +166,6 @@ export const ToolbarContent = ({ open }) => {
|
|||||||
userWallStore.logout();
|
userWallStore.logout();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayListener onClickAway={() => wallStore.setToolbarOpen(false)}>
|
<ClickAwayListener onClickAway={() => wallStore.setToolbarOpen(false)}>
|
||||||
<div className=' flex flex-col items-center w-[200px] bg-white border border-gray-300 rounded-md absolute top-0 left-8'>
|
<div className=' flex flex-col items-center w-[200px] bg-white border border-gray-300 rounded-md absolute top-0 left-8'>
|
||||||
|
@@ -3,9 +3,7 @@ import { useWallStore } from '../store/wall';
|
|||||||
import { useUserWallStore } from '../store/user-wall';
|
import { useUserWallStore } from '../store/user-wall';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { formatDate, formatRelativeDate } from '../../../modules/dayjs';
|
import { formatDate, formatRelativeDate } from '../../../modules/dayjs';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
export const List = () => {
|
export const List = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const wallStore = useUserWallStore(
|
const wallStore = useUserWallStore(
|
||||||
useShallow((state) => {
|
useShallow((state) => {
|
||||||
return {
|
return {
|
||||||
@@ -20,6 +18,9 @@ export const List = () => {
|
|||||||
const init = () => {
|
const init = () => {
|
||||||
wallStore.queryWallList();
|
wallStore.queryWallList();
|
||||||
};
|
};
|
||||||
|
const navigate = (path: string) => {
|
||||||
|
window.location.href = path;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className='p-4 bg-white w-full h-full flex flex-col'>
|
<div className='p-4 bg-white w-full h-full flex flex-col'>
|
||||||
<div
|
<div
|
||||||
@@ -37,7 +38,9 @@ export const List = () => {
|
|||||||
key={wall.id}
|
key={wall.id}
|
||||||
className='p-4 border border-gray-200 w-80 rounded-md'
|
className='p-4 border border-gray-200 w-80 rounded-md'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/edit/${wall.id}`);
|
const url = new URL(location.href);
|
||||||
|
url.searchParams.set('id', wall.id!);
|
||||||
|
navigate(url.toString());
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<div>{wall.title}</div>
|
<div>{wall.title}</div>
|
||||||
|
@@ -30,8 +30,11 @@ interface UserWallStore {
|
|||||||
wallList: Wall[];
|
wallList: Wall[];
|
||||||
queryWallList: () => Promise<void>;
|
queryWallList: () => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
saveWall: (data: Wall, opts?: { refresh?: boolean, showMessage?: boolean }) => Promise<any>;
|
saveWall: (data: Wall, opts?: { refresh?: boolean; showMessage?: boolean }) => Promise<any>;
|
||||||
queryWall: (id: string) => Promise<any>;
|
saveOneNode: (id: string, node: any) => Promise<any>;
|
||||||
|
saveDataNodes: (id: string, nodes: any[], opts?: { showMessage?: boolean }) => Promise<any>;
|
||||||
|
queryWall: (id?: string) => Promise<any>;
|
||||||
|
queryWallVersion: (id?: string) => Promise<any>;
|
||||||
deleteWall: (id: string) => Promise<any>;
|
deleteWall: (id: string) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ export const useUserWallStore = create<UserWallStore>((set, get) => ({
|
|||||||
set({ wallList: res.data.list });
|
set({ wallList: res.data.list });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveWall: async (data: Wall, opts?: { refresh?: boolean, showMessage?: boolean }) => {
|
saveWall: async (data: Wall, opts?: { refresh?: boolean; showMessage?: boolean }) => {
|
||||||
const { queryWallList } = get();
|
const { queryWallList } = get();
|
||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
@@ -81,7 +84,27 @@ export const useUserWallStore = create<UserWallStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
queryWall: async (id: string) => {
|
saveOneNode: async (id: string, node: any) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'updateNode',
|
||||||
|
data: { id, node },
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
saveDataNodes: async (id: string, nodeOperateList: any[], opts?: { showMessage?: boolean }) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'updateNodes',
|
||||||
|
data: { id, nodeOperateList },
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
opts?.showMessage && message.success('保存成功');
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
queryWall: async (id?: string) => {
|
||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
path: 'mark',
|
path: 'mark',
|
||||||
key: 'get',
|
key: 'get',
|
||||||
@@ -97,6 +120,14 @@ export const useUserWallStore = create<UserWallStore>((set, get) => ({
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
queryWallVersion: async (id?: string) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'mark',
|
||||||
|
key: 'getVersion',
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
set({ user: undefined });
|
set({ user: undefined });
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
import { create } from 'zustand';
|
import { create, StateCreator, StoreApi, UseBoundStore } from 'zustand';
|
||||||
import { XYPosition } from '@xyflow/react';
|
import { XYPosition } from '@xyflow/react';
|
||||||
import { getWallData, setWallData } from '../utils/db';
|
import { getCacheWallData, setCacheWallData } from '../utils/db';
|
||||||
import { useUserWallStore } from './user-wall';
|
import { useUserWallStore } from './user-wall';
|
||||||
import { redirectToLogin } from '@/modules/require-to-login';
|
import { redirectToLogin } from '@/modules/require-to-login';
|
||||||
import { message } from '@/modules/message';
|
import { message } from '@/modules/message';
|
||||||
import { randomId } from '../utils/random';
|
|
||||||
import { DOCS_NODE } from '../docs';
|
import { DOCS_NODE } from '../docs';
|
||||||
import { toast } from 'react-toastify';
|
import { useContextKey } from '@kevisual/system-lib/dist/web-config';
|
||||||
import { SplitButtons } from '../components/SplitToast';
|
|
||||||
type NodeData<T = { [key: string]: any }> = {
|
type NodeData<T = { [key: string]: any }> = {
|
||||||
id: string;
|
id: string;
|
||||||
position: XYPosition;
|
position: XYPosition;
|
||||||
@@ -27,23 +26,14 @@ interface WallState {
|
|||||||
// 只做传递
|
// 只做传递
|
||||||
nodes: NodeData[];
|
nodes: NodeData[];
|
||||||
setNodes: (nodes: NodeData[]) => void;
|
setNodes: (nodes: NodeData[]) => void;
|
||||||
|
saveDataNode: (nodes: NodeData[]) => Promise<void>;
|
||||||
saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>;
|
saveNodes: (nodes: NodeData[], opts?: { showMessage?: boolean }) => Promise<void>;
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
checkAndOpen: (open?: boolean, data?: any) => void;
|
checkAndOpen: (open?: boolean, data?: any) => void;
|
||||||
selectedNode: NodeData | null;
|
|
||||||
setSelectedNode: (node: NodeData | null) => void;
|
|
||||||
editValue: string;
|
|
||||||
setEditValue: (value: string, init?: boolean) => void;
|
|
||||||
hasEdited: boolean;
|
|
||||||
setHasEdited: (hasEdited: boolean) => void;
|
|
||||||
data?: any;
|
data?: any;
|
||||||
setData: (data: any) => void;
|
setData: (data: any) => void;
|
||||||
init: (id?: string | null) => Promise<void>;
|
init: (id?: string) => Promise<void>;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
setId: (id: string | null) => void;
|
setId: (id: string | null) => void;
|
||||||
loading: boolean;
|
|
||||||
setLoading: (loading: boolean) => void;
|
|
||||||
loaded: boolean | 'error';
|
loaded: boolean | 'error';
|
||||||
toolbarOpen: boolean;
|
toolbarOpen: boolean;
|
||||||
setToolbarOpen: (open: boolean) => void;
|
setToolbarOpen: (open: boolean) => void;
|
||||||
@@ -57,27 +47,47 @@ interface WallState {
|
|||||||
clearId: () => Promise<void>;
|
clearId: () => Promise<void>;
|
||||||
mouseSelect: boolean;
|
mouseSelect: boolean;
|
||||||
setMouseSelect: (mouseSelect: boolean) => void;
|
setMouseSelect: (mouseSelect: boolean) => void;
|
||||||
|
getNodeById: (id: string) => Promise<NodeData | null>;
|
||||||
|
saveNodeById: (id: string, data: any) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
export class WallStore {
|
||||||
export const useWallStore = create<WallState>((set, get) => ({
|
private storeMap: Map<string, UseBoundStore<StoreApi<WallState>>> = new Map();
|
||||||
|
constructor() {
|
||||||
|
this.crateStoreById('today');
|
||||||
|
}
|
||||||
|
crateStoreById(id: string) {
|
||||||
|
const store = create<WallState>((set, get) => ({
|
||||||
nodes: [],
|
nodes: [],
|
||||||
loading: false,
|
|
||||||
setLoading: (loading) => set({ loading }),
|
|
||||||
setNodes: (nodes) => {
|
setNodes: (nodes) => {
|
||||||
set({ nodes });
|
set({ nodes });
|
||||||
},
|
},
|
||||||
|
saveDataNode: async (nodes: NodeData[]) => {
|
||||||
|
const id = get().id;
|
||||||
|
if (!id) {
|
||||||
|
message.error('没有id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const covertData = getNodeData(nodes);
|
||||||
|
const nodeOperateList = covertData.map((item) => ({
|
||||||
|
node: item,
|
||||||
|
}));
|
||||||
|
const res = await useUserWallStore.getState().saveDataNodes(id, nodeOperateList);
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
message.success('保存成功');
|
||||||
|
} else {
|
||||||
|
message.error('保存失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
saveNodes: async (nodes: NodeData[], opts) => {
|
saveNodes: async (nodes: NodeData[], opts) => {
|
||||||
const showMessage = opts?.showMessage ?? true;
|
const showMessage = opts?.showMessage ?? true;
|
||||||
set({ hasEdited: false });
|
const id = get().id;
|
||||||
if (!get().id) {
|
if (!id) {
|
||||||
|
message.error('没有id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const covertData = getNodeData(nodes);
|
const covertData = getNodeData(nodes);
|
||||||
setWallData({ nodes: covertData });
|
|
||||||
showMessage && message.success('保存到本地');
|
|
||||||
} else {
|
|
||||||
const { id } = get();
|
|
||||||
const userWallStore = useUserWallStore.getState();
|
const userWallStore = useUserWallStore.getState();
|
||||||
if (id) {
|
|
||||||
const covertData = getNodeData(nodes);
|
|
||||||
const res = await userWallStore.saveWall({
|
const res = await userWallStore.saveWall({
|
||||||
id,
|
id,
|
||||||
data: {
|
data: {
|
||||||
@@ -90,75 +100,59 @@ export const useWallStore = create<WallState>((set, get) => ({
|
|||||||
message.success('保存成功', {
|
message.success('保存成功', {
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
});
|
});
|
||||||
|
const markRes = res.data;
|
||||||
|
setCacheWallData(markRes, markRes?.id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
open: false,
|
|
||||||
setOpen: (open) => {
|
|
||||||
set({ open });
|
|
||||||
},
|
},
|
||||||
checkAndOpen: (open, data) => {
|
checkAndOpen: (open, data) => {
|
||||||
const state = get();
|
//
|
||||||
if (state.hasEdited || state.open) {
|
|
||||||
toast(SplitButtons, {
|
|
||||||
closeButton: false,
|
|
||||||
className: 'p-0 w-[400px] border border-purple-600/40',
|
|
||||||
ariaLabel: 'Email received',
|
|
||||||
onClose: (reason) => {
|
|
||||||
if (reason === 'success') {
|
|
||||||
set({ open: true, selectedNode: data, hasEdited: false });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else set({ open, selectedNode: data });
|
|
||||||
},
|
|
||||||
selectedNode: null,
|
|
||||||
setSelectedNode: (node) => set({ selectedNode: node }),
|
|
||||||
editValue: '',
|
|
||||||
setEditValue: (value, init = false) => {
|
|
||||||
set({ editValue: value });
|
|
||||||
if (!init) {
|
|
||||||
set({ hasEdited: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasEdited: false,
|
|
||||||
setHasEdited: (hasEdited) => set({ hasEdited }),
|
|
||||||
data: null,
|
data: null,
|
||||||
setData: (data) => set({ data }),
|
setData: (data) => set({ data }),
|
||||||
id: null,
|
id: null,
|
||||||
setId: (id) => set({ id }),
|
setId: (id) => set({ id }),
|
||||||
loaded: false,
|
loaded: false,
|
||||||
init: async (id?: string | null) => {
|
init: async (id?: string) => {
|
||||||
// 如果登陆了且如果有id,从服务器获取
|
// 如果登陆了且如果有id,从服务器获取
|
||||||
// 没有id,获取缓存
|
// 没有id,获取缓存
|
||||||
const hasLogin = localStorage.getItem('token');
|
const hasLogin = localStorage.getItem('token');
|
||||||
if (hasLogin && id) {
|
const checkVersion = async (): Promise<{ id: string; version: number } | null> => {
|
||||||
|
const res = await useUserWallStore.getState().queryWallVersion(id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
const data = res.data;
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
message.error('获取失败,请稍后刷新重试');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getNew = async () => {
|
||||||
const res = await useUserWallStore.getState().queryWall(id);
|
const res = await useUserWallStore.getState().queryWall(id);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
set({ nodes: res.data?.data?.nodes || [], loaded: true, id, data: res.data });
|
const data = res.data;
|
||||||
} else {
|
set({ nodes: data?.data?.nodes || [], loaded: true, id: data?.id, data });
|
||||||
// message.error('获取失败,请稍后刷新重试');
|
setCacheWallData(data, data?.id);
|
||||||
set({ loaded: 'error' });
|
|
||||||
}
|
}
|
||||||
} else if (!hasLogin && id) {
|
};
|
||||||
// 没有登陆,但是有id,从服务器获取
|
if (hasLogin) {
|
||||||
|
const cvData = await checkVersion();
|
||||||
|
if (cvData) {
|
||||||
|
const id = cvData?.id;
|
||||||
|
const cacheData = await getCacheWallData(id);
|
||||||
|
if (cacheData) {
|
||||||
|
const version = cacheData?.version;
|
||||||
|
if (version === cvData?.version) {
|
||||||
|
set({ nodes: cacheData?.data?.nodes || [], loaded: true, id, data: cacheData });
|
||||||
|
} else {
|
||||||
|
getNew();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getNew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// 跳转到登陆页面
|
// 跳转到登陆页面
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
} else {
|
|
||||||
const data = await getWallData();
|
|
||||||
const nodes = data?.nodes || [];
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
set({
|
|
||||||
nodes: [...DOCS_NODE], //
|
|
||||||
loaded: true,
|
|
||||||
id: null,
|
|
||||||
data: null,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set({ nodes, loaded: true, id: null, data: null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toolbarOpen: false,
|
toolbarOpen: false,
|
||||||
@@ -168,25 +162,27 @@ export const useWallStore = create<WallState>((set, get) => ({
|
|||||||
formDialogData: null,
|
formDialogData: null,
|
||||||
setFormDialogData: (data) => set({ formDialogData: data }),
|
setFormDialogData: (data) => set({ formDialogData: data }),
|
||||||
clear: async () => {
|
clear: async () => {
|
||||||
if (get().id) {
|
// if (get().id) {
|
||||||
set({ nodes: [], selectedNode: null, editValue: '', data: null });
|
// set({ nodes: [], data: null });
|
||||||
await useUserWallStore.getState().saveWall({
|
// await useUserWallStore.getState().saveWall({
|
||||||
id: get().id!,
|
// id: get().id!,
|
||||||
data: {
|
// data: {
|
||||||
nodes: [],
|
// nodes: [],
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null });
|
// set({ nodes: [], id: null, data: null });
|
||||||
await setWallData({ nodes: [] });
|
// await setCacheWallData({ nodes: [] });
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
clearId: async () => {
|
clearId: async () => {
|
||||||
set({ id: null, data: null });
|
set({ id: null, data: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
exportWall: async (nodes: NodeData[]) => {
|
exportWall: async (nodes: NodeData[]) => {
|
||||||
const covertData = getNodeData(nodes);
|
const covertData = getNodeData(nodes);
|
||||||
setWallData({ nodes: covertData });
|
const mark = get().data;
|
||||||
|
setCacheWallData({ ...mark, data: { ...mark.data, nodes: covertData } }, mark?.id);
|
||||||
// 导出为json
|
// 导出为json
|
||||||
const json = JSON.stringify(covertData);
|
const json = JSON.stringify(covertData);
|
||||||
const blob = new Blob([json], { type: 'application/json' });
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
@@ -197,8 +193,47 @@ export const useWallStore = create<WallState>((set, get) => ({
|
|||||||
a.click();
|
a.click();
|
||||||
},
|
},
|
||||||
clearQueryWall: async () => {
|
clearQueryWall: async () => {
|
||||||
set({ nodes: [], id: null, selectedNode: null, editValue: '', data: null, toolbarOpen: false, loaded: false });
|
set({ nodes: [], id: null, data: null, toolbarOpen: false, loaded: false });
|
||||||
},
|
},
|
||||||
mouseSelect: true,
|
mouseSelect: true,
|
||||||
setMouseSelect: (mouseSelect) => set({ mouseSelect }),
|
setMouseSelect: (mouseSelect) => set({ mouseSelect }),
|
||||||
}));
|
getNodeById: async (id: string) => {
|
||||||
|
const data = await getCacheWallData(get().id!);
|
||||||
|
const nodes = data?.data?.nodes || [];
|
||||||
|
return nodes.find((node) => node.id === id);
|
||||||
|
},
|
||||||
|
saveNodeById: async (id: string, data: any) => {
|
||||||
|
let node = await get().getNodeById(id);
|
||||||
|
if (node) {
|
||||||
|
node.data = {
|
||||||
|
...node.data,
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date().getTime(),
|
||||||
|
};
|
||||||
|
const newNodes = get().nodes.map((item) => {
|
||||||
|
if (item.id === id) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
set({
|
||||||
|
nodes: newNodes,
|
||||||
|
});
|
||||||
|
get().saveNodes(newNodes, { showMessage: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
this.storeMap.set(id, store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
getStoreById(id: string) {
|
||||||
|
const store = this.storeMap.get(id);
|
||||||
|
if (!store) {
|
||||||
|
return this.crateStoreById(id);
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// export const useWallStore =
|
||||||
|
const wallStore = useContextKey('wallStore', () => new WallStore());
|
||||||
|
export const useWallStore = wallStore.getStoreById('today');
|
||||||
|
@@ -2,15 +2,19 @@ import { MyCache } from '@kevisual/cache';
|
|||||||
|
|
||||||
const cache = new MyCache('cacheWall');
|
const cache = new MyCache('cacheWall');
|
||||||
|
|
||||||
export async function getWallData() {
|
export async function getCacheWallData(key?: string) {
|
||||||
const data = await cache.getData();
|
try {
|
||||||
|
const data = await cache.get(key ?? 'cacheWall');
|
||||||
return data;
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
cache.del();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setWallData(data: any) {
|
export async function setCacheWallData(data: any, key?: string) {
|
||||||
await cache.setData(data);
|
await cache.set(key ?? 'cacheWall', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearWallData() {
|
export async function clearCacheWallData() {
|
||||||
await cache.del();
|
await cache.del();
|
||||||
}
|
}
|
||||||
|
84
src/routes.tsx
Normal file
84
src/routes.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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 { AiEditor } from './pages/editor/index.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: 'nodeRender',
|
||||||
|
description: '获取编辑器',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = { lib: NodeTextEditor, type: 'react', Panels };
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'editor',
|
||||||
|
key: 'render',
|
||||||
|
description: '获取编辑器',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
ctx.body = { lib: AiEditor, type: 'react', Panels };
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'editor',
|
||||||
|
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);
|
5
template/ai-app/AiApp.tsx
Normal file
5
template/ai-app/AiApp.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const AiApp = () => {
|
||||||
|
return <div>AiApp</div>;
|
||||||
|
};
|
22
template/ai-app/main.tsx
Normal file
22
template/ai-app/main.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { app, initAIAppRootOrCreate, useContextKey } from '../app';
|
||||||
|
import { Panels } from '@/modules/panels/index';
|
||||||
|
initAIAppRootOrCreate();
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'ai',
|
||||||
|
key: 'render',
|
||||||
|
description: '渲染AI应用',
|
||||||
|
run: async (ctx) => {
|
||||||
|
const root = initAIAppRootOrCreate();
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const aiRoot = createRoot(root!);
|
||||||
|
aiRoot.render(<Panels />);
|
||||||
|
useContextKey('aiRoot', () => aiRoot, true);
|
||||||
|
ctx.body = 'aiRoot';
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addTo(app);
|
@@ -1,3 +1,40 @@
|
|||||||
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 rootEl = document.getElementById('root') as HTMLElement;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
121
template/command/routes.ts
Normal file
121
template/command/routes.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import TurndownService from 'turndown';
|
||||||
|
import { app, message } from '../app';
|
||||||
|
|
||||||
|
// 命令规则
|
||||||
|
// 1. 命令以 ! 开头
|
||||||
|
// 2. 命令和内容之间用空格隔开
|
||||||
|
// 3. 多余的地方不要有!,如果有,使用\! 代替
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// test命令 !a 显示内容 !b 但是会计法 !c 飒短发 !fdsaf s !kong !d d!!的身份 ! 是的! !ene
|
||||||
|
// 7个
|
||||||
|
export function parseCommands(text: string) {
|
||||||
|
//文本以\!的内容都去掉
|
||||||
|
text = text.replace(/\\!/g, '__REPLACE__RETURN__');
|
||||||
|
const result: { command: string; content: string }[] = [];
|
||||||
|
const parts = text.split('!');
|
||||||
|
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
const part = parts[i].trim();
|
||||||
|
if (part.length === 0) continue; // 忽略空的部分
|
||||||
|
|
||||||
|
const spaceIndex = part.indexOf(' ');
|
||||||
|
const command = '!' + (spaceIndex === -1 ? part : part.slice(0, spaceIndex));
|
||||||
|
let content = spaceIndex === -1 ? '' : part.slice(spaceIndex + 1).trim();
|
||||||
|
if (content.includes('__REPLACE__RETURN__')) {
|
||||||
|
content = content.replace('__REPLACE__RETURN__', '!');
|
||||||
|
}
|
||||||
|
result.push({ command, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'command',
|
||||||
|
key: 'handle',
|
||||||
|
description: '处理命令',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { html } = ctx.query;
|
||||||
|
// 解析 文本中的 !command 命令
|
||||||
|
// 1. 当没有命令的时候是保存文本
|
||||||
|
// 2. 当有命令的时候,查询命令,执行
|
||||||
|
// - 当命令不存在,直接返回提示
|
||||||
|
// - 当命令存在,执行命令
|
||||||
|
const turndown = new TurndownService();
|
||||||
|
const markdown = turndown.turndown(html);
|
||||||
|
const commands = parseCommands(markdown);
|
||||||
|
|
||||||
|
if (commands.length === 0) {
|
||||||
|
ctx.body = markdown;
|
||||||
|
const res = await app.call({ path: 'note', key: 'save', payload: { html } });
|
||||||
|
if (res.code !== 200) {
|
||||||
|
message.error(res.message || '保存失败');
|
||||||
|
ctx.throw(400, res.message || '保存失败');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('md', markdown);
|
||||||
|
console.log('commands', commands, commands.length);
|
||||||
|
const res = await app.call({ path: 'command', key: 'list', payload: { commands } });
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'command',
|
||||||
|
key: 'list',
|
||||||
|
description: '命令列表',
|
||||||
|
metadata: {
|
||||||
|
command: 'command-list',
|
||||||
|
prompt: '把当前我的数据中,所有命令列表返回',
|
||||||
|
},
|
||||||
|
validator: {
|
||||||
|
commands: {
|
||||||
|
type: 'any',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { commands } = ctx.query;
|
||||||
|
const getRouteInfo = (route: any) => {
|
||||||
|
return {
|
||||||
|
path: route.path,
|
||||||
|
key: route.key,
|
||||||
|
description: route.description,
|
||||||
|
metadata: route.metadata,
|
||||||
|
validator: route.validator,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (Array.isArray(commands) && commands.length > 0) {
|
||||||
|
const routes = ctx.queryRouter.routes;
|
||||||
|
const commandRoutes = commands.map((command) => {
|
||||||
|
const route = routes.find((route) => route.metadata?.command === command.command);
|
||||||
|
if (!route) {
|
||||||
|
message.error(`命令 ${command.command} 不存在`);
|
||||||
|
ctx.throw(400, `命令 ${command.command} 不存在`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
route: getRouteInfo(route),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
ctx.body = commandRoutes;
|
||||||
|
} else {
|
||||||
|
ctx.body = ctx.queryRouter.routes
|
||||||
|
.map((route) => ({
|
||||||
|
command: route.metadata?.command,
|
||||||
|
route: getRouteInfo(route),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.command);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const res = await app.call({ path: 'command', key: 'list' });
|
||||||
|
console.log('list', res.body);
|
||||||
|
}, 2000);
|
42
template/index.ts
Normal file
42
template/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { app, page, load } from './app';
|
||||||
|
import '../src/routes';
|
||||||
|
import './ai-app/main';
|
||||||
|
import './tailwind.css';
|
||||||
|
import './workspace/entry';
|
||||||
|
import './routes';
|
||||||
|
|
||||||
|
page.addPage('/', 'workspace');
|
||||||
|
|
||||||
|
const runLoad = () => {
|
||||||
|
load.load(
|
||||||
|
() => {
|
||||||
|
console.log('runLoad');
|
||||||
|
// @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()
|
2
template/routes.ts
Normal file
2
template/routes.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import './user/route';
|
||||||
|
import './command/routes';
|
30
template/tailwind.css
Normal file
30
template/tailwind.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@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 border border-gray-300 shadow-md;
|
||||||
|
.tiptap {
|
||||||
|
@apply mx-0 h-full overflow-y-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
@apply w-full h-full overflow-hidden;
|
||||||
|
}
|
||||||
|
}
|
45
template/user/route.ts
Normal file
45
template/user/route.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { app, message } from '../app';
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'user',
|
||||||
|
key: 'login',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { username, password } = ctx.query;
|
||||||
|
if (!username || !password) {
|
||||||
|
message.error('用户名和密码不能为空');
|
||||||
|
ctx.throw(400, '用户名和密码不能为空');
|
||||||
|
}
|
||||||
|
const res = await fetch('/api/router', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path: 'user', key: 'login', username, password }),
|
||||||
|
}).then((res) => res.json());
|
||||||
|
if (res.code === 200) {
|
||||||
|
localStorage.setItem('token', res.data.token);
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
ctx.throw(400, res.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'user',
|
||||||
|
key: 'logout',
|
||||||
|
description: '退出登录',
|
||||||
|
metadata: {
|
||||||
|
command: 'logout',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
fetch('/api/router?path=user&key=logout', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/user/login';
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.addTo(app);
|
50
template/user/store/index.ts
Normal file
50
template/user/store/index.ts
Normal 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;
|
||||||
|
},
|
||||||
|
}));
|
25
template/wallnote/entry.ts
Normal file
25
template/wallnote/entry.ts
Normal 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);
|
30
template/workspace/entry.ts
Normal file
30
template/workspace/entry.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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');
|
||||||
|
console.log('workspace enter', isRender);
|
||||||
|
if (!isRender) {
|
||||||
|
const res = await app.call({
|
||||||
|
path: 'wallnote',
|
||||||
|
key: 'render',
|
||||||
|
});
|
||||||
|
console.log('res', res);
|
||||||
|
isRender = true;
|
||||||
|
}
|
||||||
|
ctx.body = '';
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addTo(app);
|
36
template/workspace/prompts/html示例.md
Normal file
36
template/workspace/prompts/html示例.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
把当前我的数据中,所有的title和description和path和key列出来,生成一个好看的卡片式的列表。只给我返回html的内容,其他的东西不返回给我。
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"command": "logout",
|
||||||
|
"route": {
|
||||||
|
"path": "user",
|
||||||
|
"key": "logout",
|
||||||
|
"description": "退出登录",
|
||||||
|
"metadata": {
|
||||||
|
"command": "logout"
|
||||||
|
},
|
||||||
|
"validator": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "command-list",
|
||||||
|
"route": {
|
||||||
|
"path": "command",
|
||||||
|
"key": "list",
|
||||||
|
"description": "命令列表",
|
||||||
|
"metadata": {
|
||||||
|
"command": "command-list",
|
||||||
|
"prompt": "把当前我的数据中,所有命令列表返回"
|
||||||
|
},
|
||||||
|
"validator": {
|
||||||
|
"commands": {
|
||||||
|
"type": "any",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
6
template/workspace/prompts/命令列表.md
Normal file
6
template/workspace/prompts/命令列表.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
我有一个命令列表,我需要通过查询去获取相应的列表的内容,我提供你查询的方式。我需要你把我文本的内容转为查询的参数的格式。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
7
template/workspace/prompts/提取指令.md
Normal file
7
template/workspace/prompts/提取指令.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
我有一些命令匹配的文本,格式是: !command text-content 他是很多类同的命令结合一起的,其中text-content可能为空,其中命令和内容都可能是乱拼的,只要符合 !command ,你就要把内容返回给我。其中如果!单独存在,或者!之前面有内容,都不属于命令,都属于上一个命令的文本,你需要排出这些错误情况。你需要把命令和文本的内容返回给我一个json数据。返回的格式是[{command,content],你只需要把你对应的内容返回给我,不要返回其他内容。
|
||||||
|
|
||||||
|
我给你的命令文本是
|
||||||
|
|
||||||
|
!a 显示内容 !b 但是会计法 !c 飒短发 !fdsaf s !d d!!的身份 ! 是的! !ene
|
||||||
|
|
||||||
|
PROMPT_TEXT
|
0
template/workspace/提取指令.md
Normal file
0
template/workspace/提取指令.md
Normal file
@@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
"typings.d.ts"
|
"typings.d.ts",
|
||||||
|
"template"
|
||||||
]
|
]
|
||||||
}
|
}
|
@@ -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
35
vite.config.prompt.ts
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
@@ -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',
|
||||||
|
Reference in New Issue
Block a user