diff --git a/package-lock.json b/package-lock.json index a071142..e7d4e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,13 @@ "dayjs": "^1.11.10", "echarts": "^5.4.3", "fast-deep-equal": "^3.1.3", + "fflate": "^0.8.1", "jwt-decode": "^4.0.0", "localforage": "^1.10.0", "lodash": "^4.17.21", "match-sorter": "^6.3.1", "moment": "^2.29.4", + "monaco-editor": "^0.45.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^6.20.1", @@ -3890,6 +3892,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5263,6 +5270,11 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.45.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz", + "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 8257e58..5fc24d9 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "dayjs": "^1.11.10", "echarts": "^5.4.3", "fast-deep-equal": "^3.1.3", + "fflate": "^0.8.1", "jwt-decode": "^4.0.0", "localforage": "^1.10.0", "lodash": "^4.17.21", "match-sorter": "^6.3.1", "moment": "^2.29.4", + "monaco-editor": "^0.45.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^6.20.1", diff --git a/src/components/ReactPlayground/Playground.tsx b/src/components/ReactPlayground/Playground.tsx new file mode 100644 index 0000000..ecb7d82 --- /dev/null +++ b/src/components/ReactPlayground/Playground.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { IPlayground } from '@/components/ReactPlayground/shared.ts' +import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx' +import { ENTRY_FILE_NAME, initFiles, MAIN_FILE_NAME } from '@/components/files.ts' +import { + getCustomActiveFile, + getMergedCustomFiles, + getPlaygroundTheme +} from '@/components/ReactPlayground/Utils.ts' + +const defaultCodeSandboxOptions = { + theme: 'dark', + editorHeight: '100vh', + showUrlHash: true +} + +const Playground: React.FC = (props) => { + const { + width = '100vw', + height = '100vh', + theme, + files: propsFiles, + importMap, + showCompileOutput = true, + showHeader = true, + showFileSelector = true, + fileSelectorReadOnly = false, + border = false, + defaultSizes, + onFilesChange, + autorun = true + } = props + const { filesHash, changeTheme, files, setFiles, setSelectedFileName } = + useContext(PlaygroundContext) + const options = Object.assign(defaultCodeSandboxOptions, props.options || {}) + + useEffect(() => { + if (propsFiles && !propsFiles?.[MAIN_FILE_NAME]) { + throw new Error( + `Missing required property : '${MAIN_FILE_NAME}' is a mandatory property for 'files'` + ) + } else if (propsFiles) { + const newFiles = getMergedCustomFiles(propsFiles, importMap) + if (newFiles) { + setFiles(newFiles) + } + const selectedFileName = getCustomActiveFile(propsFiles) + if (selectedFileName) { + setSelectedFileName(selectedFileName) + } + } + }, [propsFiles]) + + useEffect(() => { + setTimeout(() => { + if (!theme) { + changeTheme(getPlaygroundTheme()) + } else { + changeTheme(theme) + } + }, 15) + }, [theme]) + + useEffect(() => { + if (!propsFiles) { + setFiles(initFiles) + } + }, []) + + return files[ENTRY_FILE_NAME] ? <> : undefined +} + +export default Playground diff --git a/src/components/ReactPlayground/Provider.tsx b/src/components/ReactPlayground/Provider.tsx new file mode 100644 index 0000000..31f6370 --- /dev/null +++ b/src/components/ReactPlayground/Provider.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts' +import { MAIN_FILE_NAME } from '@/components/files.ts' +import { + fileNameToLanguage, + setPlaygroundTheme, + strToBase64 +} from '@/components/ReactPlayground/Utils.ts' + +const initialContext: Partial = { + selectedFileName: MAIN_FILE_NAME +} + +export const PlaygroundContext = createContext( + initialContext as IPlaygroundContext +) + +interface ProviderProps extends React.PropsWithChildren { + saveOnUrl?: boolean +} + +const Provider: React.FC = ({ children, saveOnUrl }) => { + const [files, setFiles] = useState({}) + const [theme, setTheme] = useState(initialContext.theme!) + const [selectedFileName, setSelectedFileName] = useState(initialContext.selectedFileName!) + const [filesHash, setFilesHash] = useState('') + + const addFile = (name: string) => { + files[name] = { + name, + language: fileNameToLanguage(name), + value: '' + } + setFiles({ ...files }) + } + + const removeFile = (name: string) => { + delete files[name] + setFiles({ ...files }) + } + + const changeFileName = (oldFileName: string, newFileName: string) => { + if (!files[oldFileName] || !newFileName) { + return + } + + const { [oldFileName]: value, ...other } = files + const newFile: IFiles = { + [newFileName]: { + ...value, + language: fileNameToLanguage(newFileName), + name: newFileName + } + } + setFiles({ + ...other, + ...newFile + }) + } + + const changeTheme = (theme: ITheme) => { + setPlaygroundTheme(theme) + setTheme(theme) + } + + useEffect(() => { + const hash = strToBase64(JSON.stringify(files)) + if (saveOnUrl) { + window.location.hash = hash + } + setFilesHash(hash) + }, [files]) + + return ( + + {children} + + ) +} + +export default Provider diff --git a/src/components/ReactPlayground/Utils.ts b/src/components/ReactPlayground/Utils.ts new file mode 100644 index 0000000..77f4df4 --- /dev/null +++ b/src/components/ReactPlayground/Utils.ts @@ -0,0 +1,113 @@ +import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared.ts' +import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate' +import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/files.ts' + +export const strToBase64 = (str: string) => { + const buffer = strToU8(str) + const zipped = zlibSync(buffer, { level: 9 }) + const binary = strFromU8(zipped, true) + return btoa(binary) +} + +export const base64ToStr = (base64: string) => { + const binary = atob(base64) + + // zlib header (x78), level 9 (xDA) + if (binary.startsWith('\x78\xDA')) { + const buffer = strToU8(binary, true) + const unzipped = unzlibSync(buffer) + return strFromU8(unzipped) + } + + return '' +} + +export const fileNameToLanguage = (name: string) => { + const suffix = name.split('.').pop() || '' + if (['js', 'jsx'].includes(suffix)) return 'javascript' + if (['ts', 'tsx'].includes(suffix)) return 'typescript' + if (['json'].includes(suffix)) return 'json' + if (['css'].includes(suffix)) return 'css' + return 'javascript' +} + +const STORAGE_DARK_THEME = 'react-playground-prefer-dark' + +export const setPlaygroundTheme = (theme: ITheme) => { + localStorage.setItem(STORAGE_DARK_THEME, String(theme === 'dark')) + document + .querySelectorAll('div[data-id="react-playground"]') + ?.forEach((item) => item.setAttribute('class', theme)) +} + +export const getPlaygroundTheme = () => { + const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false') + return isDarkTheme ? 'dark' : 'light' +} + +const transformCustomFiles = (files: ICustomFiles) => { + const newFiles: IFiles = {} + Object.keys(files).forEach((key) => { + const tempFile = files[key] + if (typeof tempFile === 'string') { + newFiles[key] = { + name: key, + language: fileNameToLanguage(key), + value: tempFile + } + } else { + newFiles[key] = { + name: key, + language: fileNameToLanguage(key), + value: tempFile.code, + hidden: tempFile.hidden, + active: tempFile.active + } + } + }) + + return newFiles +} + +export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMap) => { + if (!files) return null + if (importMap) { + return { + ...reactTemplateFiles, + ...transformCustomFiles(files), + [IMPORT_MAP_FILE_NAME]: { + name: IMPORT_MAP_FILE_NAME, + language: 'json', + value: JSON.stringify(importMap, null, 2) + } + } + } else { + return { + ...reactTemplateFiles, + ...transformCustomFiles(files) + } + } +} + +export const getCustomActiveFile = (files?: ICustomFiles) => { + if (!files) return null + return Object.keys(files).find((key) => { + const tempFile = files[key] + if (typeof tempFile !== 'string' && tempFile.active) { + return key + } + return null + }) +} +export const getFilesFromUrl = () => { + let files: IFiles | undefined + try { + if (typeof window !== 'undefined') { + const hash = window.location.hash + if (hash) files = JSON.parse(base64ToStr(hash?.split('#')[1])) + } + } catch (error) { + console.error(error) + } + return files +} diff --git a/src/components/ReactPlayground/index.tsx b/src/components/ReactPlayground/index.tsx new file mode 100644 index 0000000..f417e64 --- /dev/null +++ b/src/components/ReactPlayground/index.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import Provider from '@/components/ReactPlayground/Provider.tsx' +import { IPlayground } from '@/components/ReactPlayground/shared.ts' +import Playground from '@/components/ReactPlayground/Playground.tsx' + +const ReactPlayground: React.FC = (props) => { + return ( + <> + + + + + ) +} + +export default ReactPlayground diff --git a/src/components/ReactPlayground/shared.ts b/src/components/ReactPlayground/shared.ts new file mode 100644 index 0000000..3fa42c1 --- /dev/null +++ b/src/components/ReactPlayground/shared.ts @@ -0,0 +1,78 @@ +import React from 'react' +import { editor } from 'monaco-editor' + +export interface IFile { + name: string + value: string + language: string + active?: boolean + hidden?: boolean +} + +export interface IFiles { + [key: string]: IFile +} + +export type ITheme = 'light' | 'dark' + +export type IImportMap = { imports: Record } + +export interface ICustomFiles { + [key: string]: + | string + | { + code: string + active?: boolean + hidden?: boolean + } +} +export type IEditorOptions = editor.IStandaloneEditorConstructionOptions & any +export interface IEditorContainer { + showFileSelector?: boolean + fileSelectorReadOnly?: boolean + options?: IEditorOptions +} + +export interface IOutput { + showCompileOutput?: boolean +} + +export interface ISplitPane { + children?: React.ReactNode[] + defaultSizes?: number[] +} + +export type IPlayground = { + width?: string | number + height?: string | number + theme?: ITheme + importMap?: IImportMap + files?: ICustomFiles + options?: { + lineNumbers?: boolean + fontSize?: number + tabSize?: number + } + showHeader?: boolean + border?: boolean + onFilesChange?: (url: string) => void + saveOnUrl?: boolean + autorun?: boolean + // recompileDelay +} & Omit & + IOutput & + ISplitPane + +export interface IPlaygroundContext { + files: IFiles + filesHash: string + theme: ITheme + selectedFileName: string + setSelectedFileName: (fileName: string) => void + setTheme: (theme: ITheme) => void + setFiles: (files: IFiles) => void + addFile: (fileName: string) => void + removeFile: (fileName: string) => void + changeFileName: (oldFieldName: string, newFieldName: string) => void + changeTheme: (theme: ITheme) => void +} diff --git a/src/components/ReactPlayground/template/.eslintrc.cjs b/src/components/ReactPlayground/template/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/src/components/ReactPlayground/template/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/src/components/ReactPlayground/template/.gitignore b/src/components/ReactPlayground/template/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/src/components/ReactPlayground/template/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/components/ReactPlayground/template/README.md b/src/components/ReactPlayground/template/README.md new file mode 100644 index 0000000..23892a5 --- /dev/null +++ b/src/components/ReactPlayground/template/README.md @@ -0,0 +1,28 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list + gitignore diff --git a/src/components/ReactPlayground/template/import-map.json b/src/components/ReactPlayground/template/import-map.json new file mode 100644 index 0000000..fc687e0 --- /dev/null +++ b/src/components/ReactPlayground/template/import-map.json @@ -0,0 +1,6 @@ +{ + "imports": { + "react": "https://esm.sh/react@18.2.0", + "react-dom/client": "https://esm.sh/react-dom@18.2.0" + } +} diff --git a/src/components/ReactPlayground/template/index.html b/src/components/ReactPlayground/template/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/src/components/ReactPlayground/template/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/src/components/ReactPlayground/template/package.json b/src/components/ReactPlayground/template/package.json new file mode 100644 index 0000000..fdcdf71 --- /dev/null +++ b/src/components/ReactPlayground/template/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-playground", + "author": "fewismuch", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitejs/plugin-react-swc": "^3.3.2", + "eslint": "^8.45.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +} diff --git a/src/components/ReactPlayground/template/public/vite.svg b/src/components/ReactPlayground/template/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/src/components/ReactPlayground/template/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReactPlayground/template/src/App.css b/src/components/ReactPlayground/template/src/App.css new file mode 100644 index 0000000..d15241f --- /dev/null +++ b/src/components/ReactPlayground/template/src/App.css @@ -0,0 +1,65 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + line-height: 1.5; + color: rgb(255 255 255 / 87%); + text-rendering: optimizelegibility; + text-size-adjust: 100%; + background-color: #242424; + color-scheme: light dark; + font-synthesis: none; +} + +#root { + max-width: 1280px; + padding: 2rem; + margin: 0 auto; + text-align: center; +} + +body { + display: flex; + min-width: 320px; + min-height: 100vh; + margin: 0; + place-items: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + padding: 0.6em 1.2em; + font-family: inherit; + font-size: 1em; + font-weight: 500; + cursor: pointer; + background-color: #1a1a1a; + border: 1px solid transparent; + border-radius: 8px; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #fff; + } + + button { + background-color: #f9f9f9; + } +} diff --git a/src/components/ReactPlayground/template/src/App.tsx b/src/components/ReactPlayground/template/src/App.tsx new file mode 100644 index 0000000..87189c5 --- /dev/null +++ b/src/components/ReactPlayground/template/src/App.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +

Hello World

+
+ +
+ + ) +} + +export default App diff --git a/src/components/ReactPlayground/template/src/main.tsx b/src/components/ReactPlayground/template/src/main.tsx new file mode 100644 index 0000000..ad8ece1 --- /dev/null +++ b/src/components/ReactPlayground/template/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' + +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/src/components/ReactPlayground/template/tsconfig.json b/src/components/ReactPlayground/template/tsconfig.json new file mode 100644 index 0000000..a7fc6fb --- /dev/null +++ b/src/components/ReactPlayground/template/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/src/components/ReactPlayground/template/tsconfig.node.json b/src/components/ReactPlayground/template/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/src/components/ReactPlayground/template/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/src/components/ReactPlayground/template/vite.config.js b/src/components/ReactPlayground/template/vite.config.js new file mode 100644 index 0000000..f16716a --- /dev/null +++ b/src/components/ReactPlayground/template/vite.config.js @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}) diff --git a/src/components/files.ts b/src/components/files.ts new file mode 100644 index 0000000..07b762b --- /dev/null +++ b/src/components/files.ts @@ -0,0 +1,46 @@ +import importMap from './template/import-map.json?raw' +import AppCss from './template/src/App.css?raw' +import App from './template/src/App.tsx?raw' +import main from './template/src/main.tsx?raw' +import { IFiles } from '@/components/ReactPlayground/shared.ts' +import { fileNameToLanguage } from '@/components/ReactPlayground/Utils.ts' + +export const MAIN_FILE_NAME = 'App.tsx' +export const IMPORT_MAP_FILE_NAME = 'import-map.json' +export const ENTRY_FILE_NAME = 'main.tsx' + +export const initFiles: IFiles = getFilesFromUrl() || { + [ENTRY_FILE_NAME]: { + name: ENTRY_FILE_NAME, + language: fileNameToLanguage(ENTRY_FILE_NAME), + value: main + }, + [MAIN_FILE_NAME]: { + name: MAIN_FILE_NAME, + language: fileNameToLanguage(MAIN_FILE_NAME), + value: App + }, + 'App.css': { + name: 'App.css', + language: 'css', + value: AppCss + }, + [IMPORT_MAP_FILE_NAME]: { + name: IMPORT_MAP_FILE_NAME, + language: fileNameToLanguage(IMPORT_MAP_FILE_NAME), + value: importMap + } +} + +export const reactTemplateFiles = { + [ENTRY_FILE_NAME]: { + name: ENTRY_FILE_NAME, + language: fileNameToLanguage(ENTRY_FILE_NAME), + value: main + }, + [IMPORT_MAP_FILE_NAME]: { + name: IMPORT_MAP_FILE_NAME, + language: fileNameToLanguage(IMPORT_MAP_FILE_NAME), + value: importMap + } +} diff --git a/src/pages/OnlineEditor.tsx b/src/pages/OnlineEditor.tsx new file mode 100644 index 0000000..7d67ca3 --- /dev/null +++ b/src/pages/OnlineEditor.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const OnlineEditor: React.FC = () => { + return <> +} + +export default OnlineEditor diff --git a/src/router/index.tsx b/src/router/index.tsx index 84716ac..2f5880c 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -62,6 +62,13 @@ const root: RouteJsonObject[] = [ auth: true, permission: true }, + { + path: 'online-editor', + absolutePath: '/online-editor', + id: 'online-editor', + component: React.lazy(() => import('@/pages/OnlineEditor')), + name: '在线编辑器' + }, { path: '', absolutePath: '/',