diff --git a/frontend/index.html b/frontend/index.html index 1ab7dd7..fe4ffb5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,12 +1,12 @@ - - - Adminive Demo + + + Adminive Demo -
- +
+ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9500ced..b4df87c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,47 +1,58 @@ -import {useCallback, useEffect, useMemo, useState} from 'react'; import axios from 'axios'; -import {render as renderAmis} from 'amis'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {amis_fetcher} from './amis_fetcher'; +import {embed_amis} from './amis_runtime'; import type {Api_Envelope} from './types'; import './App.css'; type Amis_Schema = Record; export default function App() { - const [schema, set_schema] = useState(); - const [loading, set_loading] = useState(true); - const [error, set_error] = useState(''); - const load_schema = useCallback(async () => { - set_loading(true); - set_error(''); - try { - const response = await axios.get>('/admin/amis'); - if(response.data.status !== 0) { - throw new Error(response.data.msg || '加载后端页面描述失败'); - } - set_schema(response.data.data); - } catch(reason) { - set_error(reason instanceof Error ? reason.message : String(reason)); - } finally { - set_loading(false); - } - }, []); - useEffect(() => { - void load_schema(); - }, [load_schema]); - const amis_view = useMemo(() => schema ? renderAmis(schema as any, {}, {fetcher: amis_fetcher as any}) : null, [schema]); - return ( -
-
-
-

Adminive

-

组件画廊、字段控件、布局组合、表格操作、状态刷新和配置页面均由后端 JSON 描述驱动。

-
- -
-
- {loading &&
正在加载后端描述……
} - {!loading && error &&
{error}
} - {!loading && !error && amis_view} -
-
- ); + const [schema, set_schema] = useState(); + const [loading, set_loading] = useState(true); + const [error, set_error] = useState(''); + const amis_container = useRef(null); + const load_schema = useCallback(async () => { + set_loading(true); + set_error(''); + try { + const response = await axios.get>('/admin/amis'); + if(response.data.status !== 0) { + throw new Error(response.data.msg || '加载后端页面描述失败'); + } + set_schema(response.data.data); + } catch(reason) { + set_error(reason instanceof Error ? reason.message : String(reason)); + } finally { + set_loading(false); + } + }, []); + useEffect(() => { + void load_schema(); + }, [load_schema]); + useEffect(() => { + if(!schema || !amis_container.current || loading || error) { + return; + } + try { + const scoped = embed_amis(amis_container.current, schema, amis_fetcher); + return () => scoped.unmount(); + } catch(reason) { + set_error(reason instanceof Error ? reason.message : String(reason)); + } + }, [schema, loading, error]); + return ( +
+
+
+

Adminive

+

组件画廊、字段控件、布局组合、表格操作、状态刷新和配置页面均由后端 JSON 描述驱动。

+
+ +
+
+ {loading &&
正在加载后端描述……
} + {!loading && error &&
{error}
} + {!loading && !error &&
} +
+
+ ); } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2e6f161..4a0c9aa 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,7 +1,4 @@ import ReactDOM from 'react-dom/client'; -import 'amis/lib/themes/cxd.css'; -import 'amis/lib/helper.css'; -import 'amis/sdk/iconfont.css'; import App from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4830bcc..b41be7d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,15 +1,124 @@ import react from '@vitejs/plugin-react'; +import {readdirSync, readFileSync, statSync} from 'node:fs'; +import {extname, resolve, sep} from 'node:path'; import {fileURLToPath, URL} from 'node:url'; -import {defineConfig} from 'vite'; - +import {defineConfig, type Plugin} from 'vite'; const output_dir = fileURLToPath(new URL('../frontend_dist', import.meta.url)); const cache_dir = fileURLToPath(new URL('./node_modules/.vite-adminive', import.meta.url)); - +const amis_sdk_dir = fileURLToPath(new URL('./node_modules/amis/sdk', import.meta.url)); +const amis_sdk_url_prefix = '/vendor/amis/'; +function content_type(file_name: string) { + switch(extname(file_name).toLowerCase()) { + case '.css': + return 'text/css; charset=utf-8'; + case '.js': + return 'text/javascript; charset=utf-8'; + case '.json': + return 'application/json; charset=utf-8'; + case '.svg': + return 'image/svg+xml'; + case '.woff': + return 'font/woff'; + case '.woff2': + return 'font/woff2'; + case '.ttf': + return 'font/ttf'; + default: + return 'application/octet-stream'; + } +} +function amis_sdk_plugin(): Plugin { + return { + name: 'adminive-amis-sdk', + enforce: 'pre', + resolveId(source) { + if(source === 'amis' || source.startsWith('amis/')) { + this.error(`Do not import ${source} into the Adminive Vite module graph; use the AMIS SDK runtime instead`); + } + return null; + }, + buildStart() { + const emit_directory = (directory: string, prefix: string) => { + for(const entry of readdirSync(directory)) { + const source_path = resolve(directory, entry); + const relative_path = `${prefix}${entry}`; + if(statSync(source_path).isDirectory()) { + emit_directory(source_path, `${relative_path}/`); + continue; + } + this.emitFile({ + type: 'asset', + fileName: `vendor/amis/${relative_path}`, + source: readFileSync(source_path) + }); + } + }; + emit_directory(amis_sdk_dir, ''); + }, + transformIndexHtml: { + order: 'post', + handler() { + return [ + {tag: 'link', attrs: {rel: 'stylesheet', href: `${amis_sdk_url_prefix}sdk.css`}, injectTo: 'head-prepend'}, + {tag: 'link', attrs: {rel: 'stylesheet', href: `${amis_sdk_url_prefix}helper.css`}, injectTo: 'head-prepend'}, + {tag: 'link', attrs: {rel: 'stylesheet', href: `${amis_sdk_url_prefix}iconfont.css`}, injectTo: 'head-prepend'}, + {tag: 'script', attrs: {src: `${amis_sdk_url_prefix}sdk.js`}, injectTo: 'head-prepend'} + ]; + } + }, + configureServer(server) { + server.middlewares.use((request, response, next) => { + const pathname = new URL(request.url ?? '/', 'http://localhost').pathname; + if(!pathname.startsWith(amis_sdk_url_prefix)) { + next(); + return; + } + const relative_path = decodeURIComponent(pathname.slice(amis_sdk_url_prefix.length)); + const source_path = resolve(amis_sdk_dir, relative_path); + if(source_path !== amis_sdk_dir && !source_path.startsWith(`${amis_sdk_dir}${sep}`)) { + next(); + return; + } + try { + const source = readFileSync(source_path); + response.statusCode = 200; + response.setHeader('Content-Type', content_type(source_path)); + response.end(source); + } catch { + next(); + } + }); + }, + configurePreviewServer(server) { + server.middlewares.use((request, response, next) => { + const pathname = new URL(request.url ?? '/', 'http://localhost').pathname; + if(!pathname.startsWith(amis_sdk_url_prefix)) { + next(); + return; + } + const relative_path = decodeURIComponent(pathname.slice(amis_sdk_url_prefix.length)); + const source_path = resolve(amis_sdk_dir, relative_path); + if(source_path !== amis_sdk_dir && !source_path.startsWith(`${amis_sdk_dir}${sep}`)) { + next(); + return; + } + try { + const source = readFileSync(source_path); + response.statusCode = 200; + response.setHeader('Content-Type', content_type(source_path)); + response.end(source); + } catch { + next(); + } + }); + } + }; +} export default defineConfig({ cacheDir: cache_dir, - plugins: [react()], + plugins: [react(), amis_sdk_plugin()], resolve: { - dedupe: ['mobx', 'mobx-react', 'mobx-react-lite', 'mobx-state-tree', 'react', 'react-dom'] + dedupe: ['react', 'react-dom'] }, server: { host: '0.0.0.0', @@ -23,11 +132,6 @@ export default defineConfig({ }, build: { outDir: output_dir, - emptyOutDir: true, - rollupOptions: { - output: { - inlineDynamicImports: true - } - } + emptyOutDir: true } -}); \ No newline at end of file +});