feat: add listen code change
This commit is contained in:
@@ -3,6 +3,7 @@ import { ConfigProvider } from 'antd';
|
||||
import { App as ContainerApp } from './pages/container';
|
||||
import { App as PanelApp } from './pages/panel';
|
||||
import { App as PublishApp } from './pages/publish';
|
||||
import { App as CodeEditorApp } from './pages/code-editor';
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
@@ -17,6 +18,7 @@ export const App = () => {
|
||||
<Route path='/container/*' element={<ContainerApp />} />
|
||||
<Route path='/panel/*' element={<PanelApp />} />
|
||||
<Route path='/publish/*' element={<PublishApp />} />
|
||||
<Route path='/code-editor' element={<CodeEditorApp />} />
|
||||
<Route path='/404' element={<div>404</div>} />
|
||||
<Route path='*' element={<div>404</div>} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
import { Query } from '@kevisual/query';
|
||||
|
||||
export const query = new Query({});
|
||||
|
||||
export const request = query.post;
|
||||
export const ws = new WebSocket('ws://localhost:6010/api/router');
|
||||
import { create } from 'zustand';
|
||||
|
||||
type Store = {
|
||||
connected: boolean;
|
||||
setConnected: (connected: boolean) => void;
|
||||
};
|
||||
export const useStore = create<Store>((set) => ({
|
||||
connected: false,
|
||||
setConnected: (connected) => set({ connected }),
|
||||
}));
|
||||
|
||||
// 当连接成功时
|
||||
ws.onopen = () => {
|
||||
console.log('Connected to WebSocket server');
|
||||
useStore.getState().setConnected(true);
|
||||
};
|
||||
// 接收服务器的消息
|
||||
ws.onmessage = (event) => {
|
||||
console.log('Received message:', event.data);
|
||||
// const message = JSON.parse(event.data);
|
||||
};
|
||||
|
||||
// 处理 WebSocket 关闭
|
||||
ws.onclose = () => {
|
||||
console.log('Disconnected from WebSocket server');
|
||||
};
|
||||
|
||||
29
src/pages/code-editor/hooks/use-to-code-editor.ts
Normal file
29
src/pages/code-editor/hooks/use-to-code-editor.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useNavigate } from 'react-router';
|
||||
import { ParseData } from '../store';
|
||||
export const useToCodeEditor = () => {
|
||||
const navigate = useNavigate();
|
||||
const toPage = (pathKey: ParseData['updatePath'], data: ParseData['data']) => {
|
||||
navigate('/code-editor', {
|
||||
state: {
|
||||
updatePath: pathKey,
|
||||
data,
|
||||
},
|
||||
});
|
||||
};
|
||||
const PageUpdate = {
|
||||
path: 'page',
|
||||
key: 'update',
|
||||
};
|
||||
const toPagePage = (data: ParseData['data']) => {
|
||||
navigate('/code-editor', {
|
||||
state: {
|
||||
updatePath: PageUpdate,
|
||||
data,
|
||||
},
|
||||
});
|
||||
};
|
||||
return {
|
||||
toPage,
|
||||
toPagePage,
|
||||
};
|
||||
};
|
||||
75
src/pages/code-editor/index.tsx
Normal file
75
src/pages/code-editor/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createEditorInstance, editor } from '@kevisual/codemirror/dist/editor.json';
|
||||
import { useEffect, useRef, useLayoutEffect, useState, useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import { useCodeEditorStore, ParseData } from './store';
|
||||
import { useToCodeEditor } from './hooks/use-to-code-editor';
|
||||
export { useToCodeEditor };
|
||||
import { Button, message } from 'antd';
|
||||
export const App = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<typeof editor>(null);
|
||||
const location = useLocation();
|
||||
const store = useCodeEditorStore();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
initEditor();
|
||||
const state = location.state as ParseData;
|
||||
if (state && state.data) {
|
||||
store.init(state);
|
||||
}
|
||||
setMounted(true);
|
||||
return () => {
|
||||
setMounted(false);
|
||||
};
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
if (!mounted) return;
|
||||
if (editorRef.current) {
|
||||
setNewValue(store.code);
|
||||
} else {
|
||||
setNewValue('');
|
||||
}
|
||||
}, [store.code, mounted]);
|
||||
const initEditor = () => {
|
||||
const _editor = createEditorInstance(ref.current!);
|
||||
editorRef.current = _editor;
|
||||
};
|
||||
const setNewValue = (value: string) => {
|
||||
const editor = editorRef.current;
|
||||
if (editor) {
|
||||
editor.dispatch({
|
||||
changes: [{ from: 0, to: editor.state.doc.length, insert: value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
const getValue = () => {
|
||||
const editor = editorRef.current;
|
||||
if (editor) {
|
||||
return editor.state.doc.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const onSave = useCallback(async () => {
|
||||
const value = getValue();
|
||||
// store.onUpdate(value);
|
||||
if (store.dataType === 'object') {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('JSON format error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
store.onUpdate(value);
|
||||
}, [store.dataType]);
|
||||
return (
|
||||
<div className='w-full h-full bg-gray-400'>
|
||||
<div className='px-2 bg-white mb-4'>Code Editor</div>
|
||||
<Button onClick={onSave}>Save</Button>
|
||||
<div className='w-full p-4 rounded-md bg-white' style={{ height: 'calc(100% - 130px)' }}>
|
||||
<div className='w-full h-full' ref={ref}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
84
src/pages/code-editor/store.ts
Normal file
84
src/pages/code-editor/store.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { create } from 'zustand';
|
||||
import { produce } from 'immer';
|
||||
import { query } from '@/modules';
|
||||
|
||||
type QueryPath = {
|
||||
key?: string;
|
||||
path?: string;
|
||||
};
|
||||
export type ParseData = {
|
||||
updatePath: QueryPath;
|
||||
data?: string | Object;
|
||||
};
|
||||
type CodeEditorStore = {
|
||||
code: string;
|
||||
type: string; // javascript, json
|
||||
setCode: (code: string) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
data: any;
|
||||
dataType: string | Object;
|
||||
setData: (data: any) => void;
|
||||
updatePath: QueryPath;
|
||||
setUpdatePath: (updatePath: any) => void;
|
||||
onUpdate: (data: string) => Promise<any>;
|
||||
init: (parseData: { updatePath: QueryPath; data?: string | Object }) => void;
|
||||
};
|
||||
export const useCodeEditorStore = create<CodeEditorStore>((set, get) => ({
|
||||
code: '',
|
||||
type: 'javascript',
|
||||
setCode: (code: string) => set({ code }),
|
||||
loading: false,
|
||||
setLoading: (loading: boolean) => set({ loading }),
|
||||
data: {},
|
||||
dataType: 'json',
|
||||
setData: (data: any) => set({ data }),
|
||||
updatePath: {},
|
||||
setUpdatePath: (updatePath) => set({ updatePath }),
|
||||
onUpdate: async (data: string) => {
|
||||
const { updatePath, dataType } = get();
|
||||
const { path, key } = updatePath;
|
||||
let _newData = data;
|
||||
if (dataType === 'object') {
|
||||
try {
|
||||
_newData = JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
} else if (dataType === 'string') {
|
||||
_newData = data;
|
||||
}
|
||||
if (path && key) {
|
||||
const res = await query.post({ path, key, data: _newData });
|
||||
if (res.code === 200) {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.data = res.data;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
init({ updatePath, data }) {
|
||||
if (typeof data === 'string' && data) {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.code = data;
|
||||
state.data = data;
|
||||
state.dataType = 'string';
|
||||
state.updatePath = updatePath;
|
||||
}),
|
||||
);
|
||||
} else if (typeof data === 'object' && data) {
|
||||
set(
|
||||
produce((state) => {
|
||||
state.data = data;
|
||||
state.code = JSON.stringify(data, null, 2);
|
||||
state.dataType = 'object';
|
||||
state.updatePath = updatePath;
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -1,14 +1,60 @@
|
||||
import { Container } from '@abearxiong/container';
|
||||
import { Container, ContainerEdit, RenderData } from '@abearxiong/container';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { query } from '@/modules';
|
||||
import { replace, useParams } from 'react-router';
|
||||
import { query, ws, useStore } from '@/modules';
|
||||
import { message } from 'antd';
|
||||
|
||||
export const useListener = (id?: string, opts?: any) => {
|
||||
const { refresh } = opts || {};
|
||||
const connected = useStore((state) => state.connected);
|
||||
// 监听服务器的消息
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (!connected) return;
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
data: {
|
||||
type: 'pageEdit',
|
||||
data: {
|
||||
cid: id,
|
||||
// cids: [],
|
||||
// pids:'',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.addEventListener('message', listener);
|
||||
return () => {
|
||||
if (!id) return;
|
||||
if (!connected) return;
|
||||
ws.removeEventListener('message', listener);
|
||||
};
|
||||
}, [id, connected]);
|
||||
const listener = (event) => {
|
||||
const parseIfJson = (data: string) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
const receivedData = parseIfJson(event.data);
|
||||
if (typeof receivedData === 'string') return;
|
||||
if (receivedData.type === 'pageEdit' && receivedData.source === 'container') {
|
||||
const { data: containerData } = receivedData;
|
||||
if (containerData.id !== id) return;
|
||||
if (refresh) {
|
||||
refresh(containerData);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
export const Preview = () => {
|
||||
const params = useParams<{ id: string }>();
|
||||
const id = params.id;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<any>(null);
|
||||
const containerRef = useRef<Container | null>(null);
|
||||
const [data, setData] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,6 +74,7 @@ export const Preview = () => {
|
||||
const code = {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
codeId: data.Id,
|
||||
code: data.code,
|
||||
data: data.data,
|
||||
};
|
||||
@@ -36,14 +83,40 @@ export const Preview = () => {
|
||||
message.error(res.msg || 'Failed to fetch data');
|
||||
}
|
||||
};
|
||||
const init = async (data: any[]) => {
|
||||
const refresh = (data: any) => {
|
||||
if (!data.id) return;
|
||||
const code = {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
codeId: data.Id,
|
||||
code: data.code,
|
||||
data: data.data,
|
||||
hash: '',
|
||||
};
|
||||
init([code], true);
|
||||
};
|
||||
useListener(id, { refresh: refresh });
|
||||
const init = async (data: any[], replace: boolean = false) => {
|
||||
// console.log('data', data, ref.current);
|
||||
if (containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
console.log('data', data, container.data);
|
||||
container.updateData(data);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
console.log('update---data', data, container.data);
|
||||
container.destroy(id!);
|
||||
container.renderId(id!);
|
||||
return;
|
||||
}
|
||||
console.log('data', containerRef.current);
|
||||
const container = new Container({
|
||||
root: ref.current!,
|
||||
data: data as any,
|
||||
showChild: false,
|
||||
});
|
||||
container.render(id!);
|
||||
container.renderId(id!);
|
||||
containerRef.current = container;
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -43,22 +43,25 @@ export const Deck = () => {
|
||||
const codes = nodes.map((node: any) => {
|
||||
const container = node.container;
|
||||
const data = container?.data || {};
|
||||
const nodeData = node.data || {};
|
||||
let style = nodeData.style ?? {
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
return {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
code: container?.code || '',
|
||||
data: data,
|
||||
children: node.children,
|
||||
...data, // style className shadowRoot showChild
|
||||
// TODO: style className shadowRoot showChild
|
||||
className: nodeData.className,
|
||||
shadowRoot: nodeData.shadowRoot,
|
||||
showChild: nodeData.showChild,
|
||||
style,
|
||||
};
|
||||
});
|
||||
// const code = {
|
||||
// id: data.id,
|
||||
// title: data.title,
|
||||
// code: data.code,
|
||||
// data: data.data,
|
||||
// };
|
||||
// init([code]);
|
||||
init(codes);
|
||||
console.log('codes', codes);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { useToCodeEditor } from '@/pages/code-editor';
|
||||
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
@@ -69,6 +69,7 @@ const FormModal = () => {
|
||||
};
|
||||
export const List = () => {
|
||||
const navicate = useNavigate();
|
||||
const toCodeEditor = useToCodeEditor();
|
||||
const editStore = useEditStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
@@ -134,6 +135,12 @@ export const List = () => {
|
||||
}}>
|
||||
Deck
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
toCodeEditor.toPagePage(record);
|
||||
}}>
|
||||
Source Data Editor
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
|
||||
22
src/utils/history.ts
Normal file
22
src/utils/history.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
type To = string | Location;
|
||||
type State<T> = {
|
||||
[key: string]: any;
|
||||
} & T;
|
||||
export const push = <T = any>(to: To, state?: State<T>, refresh = true) => {
|
||||
const _history = window.history;
|
||||
if (typeof to === 'string') {
|
||||
// must key is default, so react navigate can work
|
||||
_history.pushState({ key: 'default', usr: state }, '', to);
|
||||
} else {
|
||||
// const path = to.pathname;
|
||||
_history.pushState({ key: 'default', usr: state }, '', to.pathname);
|
||||
}
|
||||
// must dispatch popstate event, so react navigate can work
|
||||
refresh && window.dispatchEvent(new Event('popstate'));
|
||||
};
|
||||
export const history = {
|
||||
push,
|
||||
};
|
||||
|
||||
// import { createBrowserHistory } from 'history';
|
||||
// export const history = createBrowserHistory();
|
||||
Reference in New Issue
Block a user