From db30d300a9e77c0d18d20afd159a2802de03f137 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 07:06:32 +0800 Subject: [PATCH 01/81] feat: Add and register the runtime-deploy toolbar --- .../src/vite-plugins/devAliasPlugin.js | 1 + packages/design-core/package.json | 1 + packages/design-core/re-export.js | 1 + packages/design-core/registry.js | 2 + packages/layout/src/defaultLayout.js | 1 + packages/register/src/constants.ts | 1 + packages/toolbars/runtime-deploy/index.ts | 19 +++++++ packages/toolbars/runtime-deploy/meta.js | 12 +++++ packages/toolbars/runtime-deploy/package.json | 41 ++++++++++++++++ packages/toolbars/runtime-deploy/src/Main.vue | 49 +++++++++++++++++++ .../toolbars/runtime-deploy/vite.config.ts | 36 ++++++++++++++ tsconfig.app.json | 1 + 12 files changed, 165 insertions(+) create mode 100644 packages/toolbars/runtime-deploy/index.ts create mode 100644 packages/toolbars/runtime-deploy/meta.js create mode 100644 packages/toolbars/runtime-deploy/package.json create mode 100644 packages/toolbars/runtime-deploy/src/Main.vue create mode 100644 packages/toolbars/runtime-deploy/vite.config.ts diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js index 67783ab22b..69b4070fce 100644 --- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js +++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js @@ -46,6 +46,7 @@ const getDevAlias = (useSourceAlias) => { '@opentiny/tiny-engine-toolbar-redoundo': path.resolve(basePath, 'packages/toolbars/redoundo/index.ts'), '@opentiny/tiny-engine-toolbar-clean': path.resolve(basePath, 'packages/toolbars/clean/index.ts'), '@opentiny/tiny-engine-toolbar-theme-switch': path.resolve(basePath, 'packages/toolbars/themeSwitch/index.ts'), + '@opentiny/tiny-engine-toolbar-runtime-deploy': path.resolve(basePath, 'packages/toolbars/runtime-deploy/index.ts'), '@opentiny/tiny-engine-toolbar-save': path.resolve(basePath, 'packages/toolbars/save/index.ts'), '@opentiny/tiny-engine-toolbar-setting': path.resolve(basePath, 'packages/toolbars/setting/index.ts'), '@opentiny/tiny-engine-toolbar-collaboration': path.resolve(basePath, 'packages/toolbars/collaboration/index.ts'), diff --git a/packages/design-core/package.json b/packages/design-core/package.json index e031982a4d..4a59f0f675 100644 --- a/packages/design-core/package.json +++ b/packages/design-core/package.json @@ -78,6 +78,7 @@ "@opentiny/tiny-engine-toolbar-logo": "workspace:*", "@opentiny/tiny-engine-toolbar-media": "workspace:*", "@opentiny/tiny-engine-toolbar-preview": "workspace:*", + "@opentiny/tiny-engine-toolbar-runtime-deploy": "workspace:*", "@opentiny/tiny-engine-toolbar-redoundo": "workspace:*", "@opentiny/tiny-engine-toolbar-refresh": "workspace:*", "@opentiny/tiny-engine-toolbar-save": "workspace:*", diff --git a/packages/design-core/re-export.js b/packages/design-core/re-export.js index eba647cb04..7e027f9bf3 100644 --- a/packages/design-core/re-export.js +++ b/packages/design-core/re-export.js @@ -11,6 +11,7 @@ export { default as Save } from '@opentiny/tiny-engine-toolbar-save' export { default as Clean } from '@opentiny/tiny-engine-toolbar-clean' export { default as ThemeSwitch, ThemeSwitchService } from '@opentiny/tiny-engine-toolbar-theme-switch' export { default as Preview } from '@opentiny/tiny-engine-toolbar-preview' +export { default as RuntimeDeploy } from '@opentiny/tiny-engine-toolbar-runtime-deploy' export { default as GenerateCode, SaveLocalService } from '@opentiny/tiny-engine-toolbar-generate-code' export { default as Refresh } from '@opentiny/tiny-engine-toolbar-refresh' export { default as Collaboration } from '@opentiny/tiny-engine-toolbar-collaboration' diff --git a/packages/design-core/registry.js b/packages/design-core/registry.js index e0ddd14535..e7d98b9923 100644 --- a/packages/design-core/registry.js +++ b/packages/design-core/registry.js @@ -25,6 +25,7 @@ import { Clean, ThemeSwitch, Preview, + RuntimeDeploy, GenerateCode, Refresh, Collaboration, @@ -140,6 +141,7 @@ export default { __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.collaboration'] === false ? null : Collaboration, __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.clean'] === false ? null : Clean, __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.preview'] === false ? null : Preview, + __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.runtime-deploy'] === false ? null : RuntimeDeploy, __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.refresh'] === false ? null : Refresh, __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.generate-code'] === false ? null : GenerateCode, __TINY_ENGINE_REMOVED_REGISTRY['engine.toolbars.save'] === false ? null : Save, diff --git a/packages/layout/src/defaultLayout.js b/packages/layout/src/defaultLayout.js index 62a69ed670..975bdea876 100644 --- a/packages/layout/src/defaultLayout.js +++ b/packages/layout/src/defaultLayout.js @@ -26,6 +26,7 @@ export default { right: [ [META_APP.ThemeSwitch, META_APP.RedoUndo, META_APP.Clean], [META_APP.Preview], + [META_APP.RuntimeDeploy], [META_APP.GenerateCode, META_APP.Save] ], collapse: [ diff --git a/packages/register/src/constants.ts b/packages/register/src/constants.ts index d200a3d724..9886c1098d 100644 --- a/packages/register/src/constants.ts +++ b/packages/register/src/constants.ts @@ -37,6 +37,7 @@ export const META_APP = { Save: 'engine.toolbars.save', GenerateCode: 'engine.toolbars.generate-code', Preview: 'engine.toolbars.preview', + RuntimeDeploy: 'engine.toolbars.runtime-deploy', RedoUndo: 'engine.toolbars.redoundo', Fullscreen: 'engine.toolbars.fullscreen', Lock: 'engine.toolbars.lock', diff --git a/packages/toolbars/runtime-deploy/index.ts b/packages/toolbars/runtime-deploy/index.ts new file mode 100644 index 0000000000..c0e0c06878 --- /dev/null +++ b/packages/toolbars/runtime-deploy/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import entry from './src/Main.vue' +import metaData from './meta' + +export default { + ...metaData, + entry +} diff --git a/packages/toolbars/runtime-deploy/meta.js b/packages/toolbars/runtime-deploy/meta.js new file mode 100644 index 0000000000..5d310c1da4 --- /dev/null +++ b/packages/toolbars/runtime-deploy/meta.js @@ -0,0 +1,12 @@ +export default { + id: 'engine.toolbars.runtime-deploy', + type: 'toolbars', + title: 'runtime-deploy', + options: { + icon: { + default: 'video' + }, + renderType: 'icon', + deployUrl: '' + } +} diff --git a/packages/toolbars/runtime-deploy/package.json b/packages/toolbars/runtime-deploy/package.json new file mode 100644 index 0000000000..d82ee6fcd8 --- /dev/null +++ b/packages/toolbars/runtime-deploy/package.json @@ -0,0 +1,41 @@ +{ + "name": "@opentiny/tiny-engine-toolbar-runtime-deploy", + "version": "2.7.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "vite build" + }, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/opentiny/tiny-engine", + "directory": "packages/toolbars/runtime-deploy" + }, + "bugs": { + "url": "https://github.com/opentiny/tiny-engine/issues" + }, + "author": "OpenTiny Team", + "license": "MIT", + "homepage": "https://opentiny.design/tiny-engine", + "dependencies": { + "@opentiny/tiny-engine-common": "workspace:*", + "@opentiny/tiny-engine-meta-register": "workspace:*" + }, + "devDependencies": { + "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", + "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "vite": "^5.4.2" + }, + "peerDependencies": { + "@opentiny/vue": "^3.20.0", + "vue": "^3.4.15" + } +} diff --git a/packages/toolbars/runtime-deploy/src/Main.vue b/packages/toolbars/runtime-deploy/src/Main.vue new file mode 100644 index 0000000000..cf61ede9e8 --- /dev/null +++ b/packages/toolbars/runtime-deploy/src/Main.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/toolbars/runtime-deploy/vite.config.ts b/packages/toolbars/runtime-deploy/vite.config.ts new file mode 100644 index 0000000000..6e6a57963f --- /dev/null +++ b/packages/toolbars/runtime-deploy/vite.config.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { defineConfig } from 'vite' +import path from 'path' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import generateComment from '@opentiny/tiny-engine-vite-plugin-meta-comments' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [generateComment(), vue(), vueJsx()], + publicDir: false, + resolve: {}, + build: { + sourcemap: true, + lib: { + entry: path.resolve(__dirname, './index.ts'), + name: 'toolbar-runtime-deploy', + fileName: (_format, entryName) => `${entryName}.js`, + formats: ['es'] + }, + rollupOptions: { + external: ['vue', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/] + } + } +}) diff --git a/tsconfig.app.json b/tsconfig.app.json index 2554b8449f..c3cabfbfe8 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -44,6 +44,7 @@ "@opentiny/tiny-engine-toolbar-generate-code": ["packages/toolbars/generate-code/index"], "@opentiny/tiny-engine-toolbar-lang": ["packages/toolbars/lang/index"], "@opentiny/tiny-engine-toolbar-lock": ["packages/toolbars/lock/index"], + "@opentiny/tiny-engine-toolbar-runtime-deploy": ["packages/toolbars/runtime-deploy/index"], "@opentiny/tiny-engine-toolbar-logo": ["packages/toolbars/logo/index"], "@opentiny/tiny-engine-toolbar-media": ["packages/toolbars/media/index"], "@opentiny/tiny-engine-toolbar-preview": ["packages/toolbars/preview/index"], From 31c616503455ab5f762bf576911bc6350d4c265f Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 07:20:16 +0800 Subject: [PATCH 02/81] feat: Add the runtime-renderer package --- .../src/vite-plugins/devAliasPlugin.js | 1 + packages/design-core/package.json | 1 + packages/runtime-renderer/index.ts | 21 ++++++++++ packages/runtime-renderer/package.json | 39 +++++++++++++++++++ packages/runtime-renderer/src/App.vue | 3 ++ packages/runtime-renderer/vite.config.ts | 24 ++++++++++++ tsconfig.app.json | 2 + 7 files changed, 91 insertions(+) create mode 100644 packages/runtime-renderer/index.ts create mode 100644 packages/runtime-renderer/package.json create mode 100644 packages/runtime-renderer/src/App.vue create mode 100644 packages/runtime-renderer/vite.config.ts diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js index 69b4070fce..44ef9f86fc 100644 --- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js +++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js @@ -41,6 +41,7 @@ const getDevAlias = (useSourceAlias) => { '@opentiny/tiny-engine-toolbar-logo': path.resolve(basePath, 'packages/toolbars/logo/index.ts'), '@opentiny/tiny-engine-toolbar-media': path.resolve(basePath, 'packages/toolbars/media/index.ts'), '@opentiny/tiny-engine-toolbar-preview': path.resolve(basePath, 'packages/toolbars/preview/index.ts'), + '@opentiny/tiny-engine-runtime-renderer': path.resolve(basePath, 'packages/runtime-renderer/index.ts'), '@opentiny/tiny-engine-toolbar-generate-code': path.resolve(basePath, 'packages/toolbars/generate-code/index.ts'), '@opentiny/tiny-engine-toolbar-refresh': path.resolve(basePath, 'packages/toolbars/refresh/index.ts'), '@opentiny/tiny-engine-toolbar-redoundo': path.resolve(basePath, 'packages/toolbars/redoundo/index.ts'), diff --git a/packages/design-core/package.json b/packages/design-core/package.json index 4a59f0f675..1be01035a4 100644 --- a/packages/design-core/package.json +++ b/packages/design-core/package.json @@ -85,6 +85,7 @@ "@opentiny/tiny-engine-toolbar-setting": "workspace:*", "@opentiny/tiny-engine-toolbar-theme-switch": "workspace:*", "@opentiny/tiny-engine-toolbar-view-setting": "workspace:*", + "@opentiny/tiny-engine-runtime-renderer": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", "@vue/babel-plugin-jsx": "^1.2.5", diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts new file mode 100644 index 0000000000..89c0ad73a0 --- /dev/null +++ b/packages/runtime-renderer/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { createApp } from 'vue' +import App from './src/App.vue' + +// 初始化运行时渲染器 +export const initRuntimeRenderer = async () => { + const app = createApp(App) + app.mount('#app') + return app +} diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json new file mode 100644 index 0000000000..9dc4f67e77 --- /dev/null +++ b/packages/runtime-renderer/package.json @@ -0,0 +1,39 @@ +{ + "name": "@opentiny/tiny-engine-runtime-renderer", + "version": "1.0.0", + "description": "TinyEngine Runtime Renderer Package", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@opentiny/tiny-engine-builtin-component": "workspace:*", + "@opentiny/tiny-engine-i18n-host": "workspace:*", + "@vue/babel-plugin-jsx": "^1.2.5", + "@babel/core": "^7.18.13", + "@vue/shared": "^3.3.4", + "axios": "latest", + "axios-mock-adapter": "^1.19.0", + "element-plus": "2.4.2", + "pinia": "^2.1.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.0.0", + "typescript": "^5.9.2", + "vite": "^4.0.0" + }, + "peerDependencies": { + "@opentiny/vue": "^3.20.0", + "@opentiny/vue-icon": "^3.20.0", + "vue": "^3.4.15", + "vue-i18n": "^9.9.0" + } +} diff --git a/packages/runtime-renderer/src/App.vue b/packages/runtime-renderer/src/App.vue new file mode 100644 index 0000000000..7b8b46cb04 --- /dev/null +++ b/packages/runtime-renderer/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/packages/runtime-renderer/vite.config.ts b/packages/runtime-renderer/vite.config.ts new file mode 100644 index 0000000000..9a479c352b --- /dev/null +++ b/packages/runtime-renderer/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + build: { + outDir: 'dist', + lib: { + entry: resolve(__dirname, 'index.ts'), + name: 'TinyEngineRuntimeRenderer', + fileName: 'index' + }, + rollupOptions: { + external: ['vue', '@opentiny/vue'], + output: { + globals: { + vue: 'Vue', + '@opentiny/vue': 'TinyVue' + } + } + } + } +}) diff --git a/tsconfig.app.json b/tsconfig.app.json index c3cabfbfe8..4beafe26ed 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -18,6 +18,8 @@ "@opentiny/tiny-engine": ["packages/design-core/index.js"], "@opentiny/tiny-engine-canvas": ["packages/canvas/index"], "@opentiny/tiny-engine-canvas/*": ["packages/canvas/*"], + "@opentiny/tiny-engine-runtime-renderer": ["packages/runtime-renderer/index"], + "@opentiny/tiny-engine-runtime-renderer/*": ["packages/runtime-renderer/*"], "@opentiny/tiny-engine-common": ["packages/common/index"], "@opentiny/tiny-engine-common/*": ["packages/common/*"], "@opentiny/tiny-engine-i18n-host": ["packages/i18n/src/lib"], From 912c023c42f053060a22cddcc513c1ca25ee788d Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 07:23:45 +0800 Subject: [PATCH 03/81] feat: Enable to start runtime-renderer from runtime-deploy button --- designer-demo/package.json | 1 + designer-demo/runtime.html | 13 +++++++ designer-demo/src/runtime.js | 18 +++++++++ packages/common/js/runtime-deploy.js | 58 ++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 designer-demo/runtime.html create mode 100644 designer-demo/src/runtime.js create mode 100644 packages/common/js/runtime-deploy.js diff --git a/designer-demo/package.json b/designer-demo/package.json index be5e7ae796..746ea8144b 100644 --- a/designer-demo/package.json +++ b/designer-demo/package.json @@ -13,6 +13,7 @@ "dependencies": { "@opentiny/tiny-engine": "workspace:^", "@opentiny/tiny-engine-meta-register": "workspace:^", + "@opentiny/tiny-engine-runtime-renderer": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", "@opentiny/vue": "~3.20.0", "@opentiny/vue-design-smb": "~3.20.0", diff --git a/designer-demo/runtime.html b/designer-demo/runtime.html new file mode 100644 index 0000000000..f80407587c --- /dev/null +++ b/designer-demo/runtime.html @@ -0,0 +1,13 @@ + + + + + + + Runtime Render + + +
+ + + diff --git a/designer-demo/src/runtime.js b/designer-demo/src/runtime.js new file mode 100644 index 0000000000..b6fc5c5bf6 --- /dev/null +++ b/designer-demo/src/runtime.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { initRuntimeRenderer } from '@opentiny/tiny-engine-runtime-renderer' + +async function startApp() { + initRuntimeRenderer() +} + +startApp() diff --git a/packages/common/js/runtime-deploy.js b/packages/common/js/runtime-deploy.js new file mode 100644 index 0000000000..c755d414dd --- /dev/null +++ b/packages/common/js/runtime-deploy.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { getMergeMeta } from '@opentiny/tiny-engine-meta-register' +import { isDevelopEnv } from './environments' + +let runtimeWindow = null +const getQueryParams = () => { + const paramsMap = new URLSearchParams(location.search) + const tenant = paramsMap.get('tenant') || '' + const platform = getMergeMeta('engine.config')?.platformId + const appId = paramsMap.get('id') + + let query = `id=${appId}&tenant=${tenant}&platform=${platform}` + return query +} + +export const deployPage = async () => { + const href = window.location.href.split('?')[0] || './' + const query = getQueryParams() + + const customDeployUrl = getMergeMeta('engine.toolbars.runtimeDeploy')?.options?.deployUrl + const defaultDeployUrl = isDevelopEnv ? `./runtime.html` : `${href.endsWith('/') ? href : `${href}/`}runtime` + + let openUrl = '' + openUrl = customDeployUrl + ? typeof customDeployUrl === 'function' + ? customDeployUrl(defaultDeployUrl, query) + : `${customDeployUrl}?${query}` + : `${defaultDeployUrl}?${query}` + + return { openUrl } +} + +export const runtimeDeploy = async () => { + const { openUrl } = await deployPage() + + if (runtimeWindow && !runtimeWindow.closed) { + try { + runtimeWindow.focus() + } catch (e) { + // eslint-disable-next-line no-console + console.warn('[runtime-deploy] focus runtime window failed:', e) + } + return + } + + runtimeWindow = window.open(openUrl, 'tiny-engine-runtime') +} From 2d70094429fd057073f415fdd6bdbed3211ce31c Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 08:28:17 +0800 Subject: [PATCH 04/81] feat: Add type definitions for config and schema --- packages/runtime-renderer/src/types/config.ts | 30 ++ packages/runtime-renderer/src/types/schema.ts | 352 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 packages/runtime-renderer/src/types/config.ts create mode 100644 packages/runtime-renderer/src/types/schema.ts diff --git a/packages/runtime-renderer/src/types/config.ts b/packages/runtime-renderer/src/types/config.ts new file mode 100644 index 0000000000..dfb645c7a1 --- /dev/null +++ b/packages/runtime-renderer/src/types/config.ts @@ -0,0 +1,30 @@ +import type { RouteLocationRaw } from 'vue-router' + +export interface PageRendererProps { + pageId: number +} + +export interface RouteConfig { + path: string + name: string + children?: RouteConfig[] + props?: PageRendererProps + redirect?: RouteLocationRaw + meta: { + pageId: number + pageName: string + hasChildren: boolean + isHome: boolean + depth: number + isDefault: boolean + hasDefault: boolean + defaultPath: string + } +} + +export interface StoreConfig { + id: string + state: Record + actions: Record + getters: Record +} diff --git a/packages/runtime-renderer/src/types/schema.ts b/packages/runtime-renderer/src/types/schema.ts new file mode 100644 index 0000000000..de6d0d0a9e --- /dev/null +++ b/packages/runtime-renderer/src/types/schema.ts @@ -0,0 +1,352 @@ +// 应用级Schema类型定义 +export interface AppSchema { + data: { + bridge: any[] + componentsMap: ComponentMap[] + componentsTree: PageSchema[] + css: string + dataSource: DataSourceConfig + utils: Util[] + packages: PackageConfig[] + meta: AppMeta + config: AppConfig + constants: string + i18n: I18nConfig + version: string + } + code: string + message: string + error: any + errMsg: any + success: boolean +} + +// 组件映射表 +export interface ComponentMap { + package?: string | null + destructuring: boolean + exportName?: string | null + componentName: string + version: string + npmrc?: any + path?: string + dependencies?: { + scripts: Array<{ + package?: string + components: Record + }> + styles: any[] + } +} + +// 页面Schema +export interface PageSchema { + children: ComponentNode[] + css: string + componentName: string + fileName: string + lifeCycles?: LifeCycles | null + meta: PageMeta + methods: Record + props: Record + state: Record +} + +export interface PageContent { + children: ComponentNode[] + css: string + componentName: string + fileName: string + lifeCycles?: LifeCycles | null + methods: Record + props: Record + state: Record +} + +// 组件节点 +export interface ComponentNode { + componentName: string + props: Record + children: ComponentNode[] | string + id: string + componentType?: string + loop?: JSExpression +} + +// 页面元信息 +export interface PageMeta { + app: number + gmt_create: string + lastUpdatedBy: string + creator: string + rootElement: boolean + contentBlocks: any[] + isHome: boolean + occupier: UserInfo + gmt_modified: string + parentId: string + occupierBy: string + isDefault: boolean + router: string + depth: number + tenantId: string + name: string + page_content?: any + id: number + isPage: boolean + group: string +} + +// 用户信息 +export interface UserInfo { + id: string + createdBy: string + lastUpdatedBy: string + tenantId: string + siteId: string + username: string + email: string + isAdmin: boolean + created_at: string + updated_at: string +} + +// 生命周期钩子 +export interface LifeCycles { + setup?: JSFunction + onBeforeMount?: JSFunction + onMounted?: JSFunction + onUpdated?: JSFunction + onBeforeUpdate?: JSFunction + onBeforeUnmount?: JSFunction + onUnmounted?: JSFunction + onErrorCaptured?: JSFunction + onActivated?: JSFunction + onDeactivated?: JSFunction +} + +// JS函数 +export interface JSFunction { + type: 'JSFunction' + value: string +} + +// JS表达式 +export interface JSExpression { + type: 'JSExpression' + value: string + model?: boolean +} + +// 数据源配置 +export interface DataSourceConfig { + dataHandler?: JSFunction + list: DataSourceItem[] +} + +// 数据源项 +export interface DataSourceItem { + id: number + name: string + data: { + columns: DataSourceColumn[] + data: any[] + type: string + options?: { + method: string + uri: string + } + dataHandler?: JSFunction + willFetch?: JSFunction + shouldFetch?: JSFunction + errorHandler?: JSFunction + } + tpl?: any + app: number + platformId: number + description?: any + created_by: string + last_updated_by: string + created_at: string + updated_at: string +} + +// 数据源列 +export interface DataSourceColumn { + name: string + title: string + field: string + type: string + format: any +} + +// 工具函数 +export interface Util { + name: string + type: string + content: Record +} + +// 包配置 +export interface PackageConfig { + name: string + version: string + script: string + css: string + others: any + package: string +} + +// 应用元信息 +export interface AppMeta { + appId: string + branch: string + creator: string + description: string + name: string + gitGroup?: any + globalState: GlobalState[] + projectName?: any + tenant?: any + gmtCreate: string + gmtModified: string + isDemo?: any +} + +// 全局状态 +export interface GlobalState { + id: string + state: Record + getters: Record + actions: Record +} + +// 应用配置 +export interface AppConfig { + targetRootID: string + sdkVersion: string + historyMode: string +} + +// 国际化配置 +export interface I18nConfig { + en_US: Record + zh_CN: Record +} + +// 区块Schema类型定义 +export interface BlockSchema { + data: BlockItem[] + code: string + message: string + error: any + errMsg: any + success: boolean +} + +// 区块项 +export interface BlockItem { + id: number + createdBy: string + lastUpdatedBy: string + tenantId: string + renterId?: any + siteId?: any + label: string + framework: string + content: BlockContent + assets: any + description?: any + tags: any[] + screenshot?: any + path?: any + i18n: any + created_at: string + updated_at: string + name_cn: string + last_build_info: { + result: boolean + versions: string[] + endTime: string + } + version: string + current_history: number + occupier: string + is_official: boolean + public: number + is_default: boolean + tiny_reserved?: any + npm_name?: any + platform_id: number + created_app: number + content_blocks?: any + public_scope_tenants: any[] + histories_length: number + is_published: boolean + current_version?: any +} + +// 区块内容 +export interface BlockContent { + componentName: string + fileName: string + css: string + props: Record + children: ComponentNode[] + schema: BlockSchemaConfig + state: Record + methods: Record + dataSource: any + dependencies: { + scripts: Array<{ + package?: string + components: Record + }> + styles: any[] + } + id: string +} + +// 区块Schema配置 +export interface BlockSchemaConfig { + properties: Array<{ + label: { + zh_CN: string + } + description: { + zh_CN: string + } + collapse: { + number: number + text: { + zh_CN: string + } + } + content: Array<{ + property: string + type: string + defaultValue: any + label: { + text: { + zh_CN: string + } + } + cols: number + rules: any[] + accessor?: { + setter?: JSFunction + } + hidden: boolean + required: boolean + readOnly: boolean + disabled: boolean + widget: { + component: string + props: any + } + properties: any[] + }> + }> + events: any + slots: any +} From 6174c0dbe8d5b8b74c2ac6d0f21ccb6a68836626 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 08:30:04 +0800 Subject: [PATCH 05/81] feat: Provide some utils for runtime-renderer --- .../runtime-renderer/src/utils/data-utils.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/runtime-renderer/src/utils/data-utils.ts diff --git a/packages/runtime-renderer/src/utils/data-utils.ts b/packages/runtime-renderer/src/utils/data-utils.ts new file mode 100644 index 0000000000..462597d847 --- /dev/null +++ b/packages/runtime-renderer/src/utils/data-utils.ts @@ -0,0 +1,30 @@ +import { utils as commonUtils } from '@opentiny/tiny-engine-utils' +export const { parseFunction: generateFunction } = commonUtils + +export const reset = (obj) => { + Object.keys(obj).forEach((key) => delete obj[key]) +} + +// 规避创建function eslint报错 +export const newFn = (...argv) => { + const Fn = Function + return new Fn(...argv) +} + +// 用于解析store中的actions和getters +export const parseJSFunction = (data: any, _scope: any = null, _ctx: any = null) => { + try { + const fn = newFn(`return ${data.value}`).call(null) // 拿到函数本体,不绑定任何 this + return fn + } catch (error) { + // eslint-disable-next-line no-console + console.error('函数声明解析报错:', error, data) + } +} + +export const getDeletedKeys = (objA, objB) => { + const keyA = Object.keys(objA) + const keyB = new Set(Object.keys(objB)) + + return keyA.filter((item) => !keyB.has(item)) +} From 7fdc46c1724e88fa4896d9310f5a95c3121a6320 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 08:34:47 +0800 Subject: [PATCH 06/81] feat: Provide builtin component Majority is the same as builtin components in package/canvas/renderer. But CanvasRouterLink.vue, CanvasRouterView.vue, and CanvasCollection.vue had changed. --- .../src/renderer/builtin/CanvasBox.vue | 16 ++++ .../src/renderer/builtin/CanvasCollection.vue | 27 ++++++ .../src/renderer/builtin/CanvasIcon.vue | 30 +++++++ .../src/renderer/builtin/CanvasImg.vue | 18 ++++ .../renderer/builtin/CanvasPlaceholder.vue | 26 ++++++ .../src/renderer/builtin/CanvasRouterLink.vue | 84 +++++++++++++++++++ .../src/renderer/builtin/CanvasRouterView.vue | 11 +++ .../src/renderer/builtin/CanvasSlot.vue | 22 +++++ .../src/renderer/builtin/CanvasText.vue | 18 ++++ .../src/renderer/builtin/index.ts | 33 ++++++++ 10 files changed, 285 insertions(+) create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasText.vue create mode 100644 packages/runtime-renderer/src/renderer/builtin/index.ts diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue new file mode 100644 index 0000000000..cfafcce048 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue new file mode 100644 index 0000000000..7fd1eda149 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue new file mode 100644 index 0000000000..42567318bf --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue new file mode 100644 index 0000000000..2581381c60 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue new file mode 100644 index 0000000000..7700b8755f --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue new file mode 100644 index 0000000000..408b1b743a --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue @@ -0,0 +1,84 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue new file mode 100644 index 0000000000..53d64d83f8 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue new file mode 100644 index 0000000000..2c54559765 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasText.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasText.vue new file mode 100644 index 0000000000..ec9de29928 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasText.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/index.ts b/packages/runtime-renderer/src/renderer/builtin/index.ts new file mode 100644 index 0000000000..e2a97e3de5 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/index.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import CanvasText from './CanvasText.vue' +import CanvasBox from './CanvasBox.vue' +import CanvasIcon from './CanvasIcon.vue' +import CanvasSlot from './CanvasSlot.vue' +import CanvasImg from './CanvasImg.vue' +import CanvasPlaceholder from './CanvasPlaceholder.vue' +import CanvasRouterLink from './CanvasRouterLink.vue' +import CanvasRouterView from './CanvasRouterView.vue' +import CanvasCollection from './CanvasCollection.vue' + +export { + CanvasText, + CanvasBox, + CanvasIcon, + CanvasSlot, + CanvasImg, + CanvasPlaceholder, + CanvasRouterLink, + CanvasRouterView, + CanvasCollection +} From bafceda8ae66874fee64e364286ce1a2b118ad5a Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 09:16:00 +0800 Subject: [PATCH 07/81] feat: Core of page renderer Referenced the implementation of tiny-schema-renderer repo and canvas/render --- .../src/components/Loading.vue | 84 ++++ .../src/renderer/RenderMain.ts | 122 ++++++ .../src/renderer/page-function/accessor.ts | 50 +++ .../renderer/page-function/blockContext.ts | 76 ++++ .../src/renderer/page-function/css-handler.ts | 110 +++++ .../src/renderer/page-function/index.ts | 13 + .../src/renderer/page-function/state.ts | 46 +++ .../src/renderer/parser/index.ts | 1 + .../src/renderer/parser/parser.ts | 363 +++++++++++++++++ .../runtime-renderer/src/renderer/render.ts | 384 ++++++++++++++++++ .../src/renderer/useContext.ts | 44 ++ 11 files changed, 1293 insertions(+) create mode 100644 packages/runtime-renderer/src/components/Loading.vue create mode 100644 packages/runtime-renderer/src/renderer/RenderMain.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/accessor.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/blockContext.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/css-handler.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/index.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/state.ts create mode 100644 packages/runtime-renderer/src/renderer/parser/index.ts create mode 100644 packages/runtime-renderer/src/renderer/parser/parser.ts create mode 100644 packages/runtime-renderer/src/renderer/render.ts create mode 100644 packages/runtime-renderer/src/renderer/useContext.ts diff --git a/packages/runtime-renderer/src/components/Loading.vue b/packages/runtime-renderer/src/components/Loading.vue new file mode 100644 index 0000000000..488b977c28 --- /dev/null +++ b/packages/runtime-renderer/src/components/Loading.vue @@ -0,0 +1,84 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts new file mode 100644 index 0000000000..7a81d497b5 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { h, computed, provide, nextTick, reactive, watch, defineComponent } from 'vue' +import Loading from '../components/Loading.vue' +import { parseData } from './parser/parser.ts' +import { renderer } from './render.ts' +import { setPageCss } from './page-function/css-handler.ts' +import { useState } from './page-function/state' +import useContext from './useContext.ts' +import { useAppSchema } from '../composables/useAppSchema' +import type { PageContent as Schema } from '../types/schema' + +interface Props { + pageId: number +} + +export default defineComponent({ + name: 'RenderMain', + props: { + pageId: { + type: Number, + default: 0 + } + }, + setup(props: Props) { + const { getPageById } = useAppSchema() + + const currentSchema = computed(() => { + const page = getPageById(props.pageId)?.meta?.page_content // 通过 pageId 获取最新的页面对象 + return JSON.parse(JSON.stringify(page)) + }) + + const { context, setContext, getContext } = useContext() + const reset = (obj: Record) => { + Object.keys(obj).forEach((key) => delete obj[key]) + } + provide('pageContext', context) + + const pageSchema = reactive({} as Schema) + const methods: Record = {} + const { state, setState } = useState({ getContext }) + + const setMethods = (data: Record = {}, clear?: boolean) => { + if (clear) reset(methods) + // 这里有些方法在画布还是有执行的必要的,比如说表格的renderer和formatText方法,包括一些自定义渲染函数 + Object.assign( + methods, + Object.fromEntries( + Object.keys(data).map((key) => { + return [key, parseData(data[key], {}, getContext())] + }) + ) + ) + setContext(methods) + } + + const setSchema = async (data: Schema) => { + if (!data) { + return + } + + const newSchema = JSON.parse(JSON.stringify(data)) + + const context = { + state + } + // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 + setContext(context, true) + + // 设置方法调用上下文 + setMethods(newSchema.methods, true) + + // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods + setState(newSchema.state, true) + await nextTick() + setPageCss(data.css || '', String(props.pageId) || 'render-main') + + Object.assign(pageSchema, newSchema) + } + + // 监听 schema 变化 + watch( + () => currentSchema.value, + async () => { + const schema = currentSchema.value + if (!schema || !Object.keys(schema).length) return + await setSchema(schema) + }, + { immediate: true } + ) + return { + pageSchema, + methods, + state + } + }, + render(): any { + const { pageSchema }: { pageSchema: Schema } = this as any + + // 渲染画布增加根节点,与出码和预览保持一致 + const rootChildrenSchema: any = { + componentName: 'div', + // 把页级 props(主要是 className: "page-base-style")挂到根容器 + props: { ...(pageSchema.props || {}) }, + children: pageSchema.children + } + + return this.pageSchema.children?.length + ? h(renderer, { schema: rootChildrenSchema, parent: this.pageSchema }) + : [h(Loading)] + } +}) diff --git a/packages/runtime-renderer/src/renderer/page-function/accessor.ts b/packages/runtime-renderer/src/renderer/page-function/accessor.ts new file mode 100644 index 0000000000..2cb4ed3bae --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/accessor.ts @@ -0,0 +1,50 @@ +import { watchEffect, type WatchStopHandle } from 'vue' +import { generateFunction } from '../../utils/data-utils' + +type IAccessorType = 'getter' | 'setter' +interface IAccessor { + getter: { value: string } + setter: { value: string } +} + +export function useAccessorMap(context: any) { + const generateAccessor = (type: IAccessorType, accessor: IAccessor, property: string) => { + const accessorFn = generateFunction(accessor[type].value, context) as (...args: any) => any + + return { property, accessorFn, type } + } + + // 这里缓存状态变量对应的访问器,用于watchEffect更新和取消监听 + const stateAccessorMap = new Map() + + // 缓存区块属性的访问器 + const propsAccessorMap = new Map() + + const generateStateAccessors = (type: IAccessorType, accessor: IAccessor, key: string) => { + const stateWatchEffectKey = `${key}${type}` + const { property, accessorFn } = generateAccessor(type, accessor, key) + + // 将之前已有的watchEffect取消监听,这里操作很有必要,不然会造成数据混乱 + stateAccessorMap.get(stateWatchEffectKey)?.() + + // 更新watchEffect监听 + stateAccessorMap.set( + stateWatchEffectKey, + watchEffect(() => { + try { + accessorFn() + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`状态变量${property}的访问器函数:${accessorFn.name}执行报错`, error) + } + }) + ) + } + + return { + generateAccessor, + stateAccessorMap, + propsAccessorMap, + generateStateAccessors + } +} diff --git a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts new file mode 100644 index 0000000000..fecd93325b --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { nextTick, inject } from 'vue' +import { getCSSHandler } from './css-handler.ts' +import { parseData } from '../parser/parser.ts' +import { useState } from './state.ts' +import useContext from '../useContext.ts' +import type { PageContent as Schema } from '../../types/schema.ts' +import dataSourceMap from '../../app-function/dataSource.js' +import { getUtilsAll } from '../../app-function/utils.ts' + +// 创建 context 实例的工厂函数 +export const createBlockContext = () => { + const { context, setContext, getContext } = useContext() + const stores = inject('stores') + const methods: Record = {} + const { state, setState } = useState({ getContext }) + + const setMethods = (data: Record = {}, clear?: boolean) => { + if (clear) { + Object.keys(methods).forEach((key) => delete methods[key]) + } + Object.assign( + methods, + Object.fromEntries( + Object.keys(data).map((key) => { + return [key, parseData(data[key], {}, getContext())] + }) + ) + ) + setContext(methods) + } + + const setSchema = async (data: Schema) => { + if (!data) return + + const newSchema = JSON.parse(JSON.stringify(data)) + + const contextData = { + state, + stores, + dataSourceMap, + utils: getUtilsAll() + } + setContext(contextData, true) + setMethods(newSchema.methods, true) + setState(newSchema.state, true) + await nextTick() + + const cssHandler = getCSSHandler({ enableScoped: true }) + cssHandler.setPageCss(data.css || '', `block-${data.fileName || 'unknown'}`) + + return context + } + + return { + setSchema, + getContext: () => context + } +} + +export const getBlockContext = (schema: Schema) => { + const blockContext = createBlockContext() + blockContext.setSchema(schema) + return blockContext.getContext() +} diff --git a/packages/runtime-renderer/src/renderer/page-function/css-handler.ts b/packages/runtime-renderer/src/renderer/page-function/css-handler.ts new file mode 100644 index 0000000000..81bfc10d79 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/css-handler.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +interface CSSHandlerOptions { + pageId?: string + enableScoped?: boolean + enableModernCSS?: boolean +} + +const fallbackStyleMap = new Map() +let enableScoped = true + +// 简化版的作用域处理,将CSS选择器添加页面作用域 +function processScopedCSS(key: string, css: string): string { + if (!enableScoped) { + return css + } + + // 简单的CSS作用域处理 + // 将 body, html 等全局选择器转换为作用域选择器 + return css + .replace(/body\s*{/g, `body[${key}] {`) + .replace(/html\s*{/g, `html[${key}] {`) + .replace(/\*:global\(([^)]+)\)/g, '$1') // 处理:global()语法 + .replace(/:global\(([^)]+)\)/g, '$1') // 处理:global()语法 +} + +// 使用传统方式设置样式 +function setCSS(key: string, css: string): void { + let styleElement = fallbackStyleMap.get(key) + + if (!styleElement) { + styleElement = document.createElement('style') + styleElement.setAttribute('type', 'text/css') + styleElement.setAttribute('data-te-page', key) + document.head?.appendChild(styleElement) + fallbackStyleMap.set(key, styleElement) + } + + // 处理作用域CSS + const processedCSS = enableScoped ? processScopedCSS(key, css) : css + + styleElement.textContent = processedCSS +} + +// 移除页面CSS +function removePageCss(key: string): void { + const styleElement = fallbackStyleMap.get(key) + if (styleElement && styleElement.parentNode) { + styleElement.parentNode.removeChild(styleElement) + fallbackStyleMap.delete(key) + } +} + +// 清理所有样式 +function clearAllStyles(): void { + fallbackStyleMap.forEach((_, key) => { + removePageCss(key) + }) +} + +// 设置页面CSS (保持原有API) +export function setPageCss(css: string = '', pageId?: string): void { + const cssPageId = pageId || 'default' + const key = `data-te-page-${cssPageId}` + + if (!css) { + removePageCss(key) + return + } + + setCSS(key, css) +} + +// 清理所有CSS(用于页面切换)(保持原有API) +export function clearAllPageCSS(): void { + clearAllStyles() +} + +// 获取全局CSS处理器 (保持向后兼容) +export function getCSSHandler(options?: CSSHandlerOptions): { + setPageCss: (css?: string, pageId?: string) => void + clearAllStyles: () => void + removePageCss: (key: string) => void +} { + if (options?.enableScoped !== undefined) { + enableScoped = options.enableScoped + } + + return { + setPageCss, + clearAllStyles, + removePageCss + } +} + +// 保持与之前相同的导出接口 +export default { + setPageCss, + clearAllPageCSS +} diff --git a/packages/runtime-renderer/src/renderer/page-function/index.ts b/packages/runtime-renderer/src/renderer/page-function/index.ts new file mode 100644 index 0000000000..a574641651 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export * from './css-handler' diff --git a/packages/runtime-renderer/src/renderer/page-function/state.ts b/packages/runtime-renderer/src/renderer/page-function/state.ts new file mode 100644 index 0000000000..74d9736c2a --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/state.ts @@ -0,0 +1,46 @@ +import { reactive } from 'vue' +import { getDeletedKeys } from '../../utils/data-utils' +import { isStateAccessor, parseData } from '../parser' +import { useAccessorMap } from './accessor' + +export function useState({ getContext }: { getContext: () => any }) { + const state = reactive>({}) + const { generateStateAccessors } = useAccessorMap(getContext()) + + const setState = (data: Record, clear?: boolean) => { + if (typeof data !== 'object' || data === null) { + return + } + + if (clear) { + Object.keys(state).forEach((key) => delete (state as any)[key]) + } + + // 智能删除处理:删除不再存在的状态键 + const deletedKeys = getDeletedKeys(state, data) + for (const key of deletedKeys) { + delete state[key] + } + + Object.assign(state, parseData(data, {}, getContext()) || {}) + + // 处理状态访问器 + Object.entries(data || {})?.forEach(([key, stateData]: [string, any]) => { + if (isStateAccessor(stateData)) { + const accessor = stateData.accessor + if (accessor?.getter?.value) { + generateStateAccessors('getter', accessor, key) + } + + if (accessor?.setter?.value) { + generateStateAccessors('setter', accessor, key) + } + } + }) + } + + return { + state, + setState + } +} diff --git a/packages/runtime-renderer/src/renderer/parser/index.ts b/packages/runtime-renderer/src/renderer/parser/index.ts new file mode 100644 index 0000000000..e9312eb336 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/parser/index.ts @@ -0,0 +1 @@ +export * from './parser' diff --git a/packages/runtime-renderer/src/renderer/parser/parser.ts b/packages/runtime-renderer/src/renderer/parser/parser.ts new file mode 100644 index 0000000000..91e18fcafd --- /dev/null +++ b/packages/runtime-renderer/src/renderer/parser/parser.ts @@ -0,0 +1,363 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import babelPluginJSX from '@vue/babel-plugin-jsx' +import { transformSync } from '@babel/core' +import { Notify } from '@opentiny/vue' +import i18nHost from '@opentiny/tiny-engine-i18n-host' + +interface ITypeParserDef { + type: (data) => boolean + parseFunc: (data: unknown, scope: Record, ctx: Record) => unknown +} + +const parseList: Array = [] + +const isI18nData = (data) => { + return data && data.type === 'i18n' +} + +const isJSSlot = (data) => { + return data && data.type === 'JSSlot' +} + +const isJSExpression = (data) => { + return data && data.type === 'JSExpression' +} + +const isJSFunction = (data) => { + return data && data.type === 'JSFunction' +} + +const isJSResource = (data) => { + return data && data.type === 'JSResource' +} + +const isString = (data) => { + return typeof data === 'string' +} + +const isArray = (data) => { + return Array.isArray(data) +} + +const isFunction = (data) => { + return typeof data === 'function' +} + +const isIcon = (data) => { + return data?.componentName === 'Icon' +} + +const isObject = (data) => { + return typeof data === 'object' +} + +// 判断是否是状态访问器 +export const isStateAccessor = (stateData) => + stateData?.accessor?.getter?.type === 'JSFunction' || stateData?.accessor?.setter?.type === 'JSFunction' + +// 规避创建function eslint报错 +export const newFn = (...argv) => { + const Fn = Function + return new Fn(...argv) +} + +const transformJSX = (code) => { + const res = transformSync(code, { + plugins: [ + [ + babelPluginJSX, + { + pragma: 'h' + } + ] + ] + }) + return (res.code || '') + .replace(/import \{.+\} from "vue";/, '') + .replace(/h\(_?resolveComponent\((.*?)\)/g, `h(this.getComponent($1)`) + .replace(/_?resolveComponent/g, 'h') + .replace(/_?createTextVNode\((.*?)\)/g, '$1') + .trim() +} + +const parseExpression = (data, scope, ctx, isJsx = false) => { + try { + if (data.value.indexOf('this.i18n') > -1) { + ctx.i18n = i18nHost.global.t + } else if (data.value.indexOf('t(') > -1) { + ctx.t = i18nHost.global.t + } + + const expression = isJsx ? transformJSX(data.value) : data.value + return newFn('$scope', `with($scope || {}) { return ${expression} }`).call(ctx, { + ...ctx, + ...scope, + slotScope: scope + }) + } catch (err) { + // 解析抛出异常,则再尝试解析 JSX 语法。如果解析 JSX 语法仍然出现错误,isJsx 变量会确保不会再次递归执行解析 + if (!isJsx) { + return parseExpression(data, scope, ctx, true) + } + return undefined + } +} + +const parseI18n = (i18n, scope, ctx) => { + return parseExpression( + { + type: 'JSExpression', + value: `this.i18n('${i18n.key}', ${JSON.stringify(i18n.params)})` + }, + scope, + { i18n: i18nHost.global.t, ...ctx } + ) +} + +// 解析函数字符串结构 +const parseFunctionString = (fnStr) => { + const fnRegexp = /(async)?.*?(\w+) *\(([\s\S]*?)\) *\{([\s\S]*)\}/ + const result = fnRegexp.exec(fnStr) + if (result) { + return { + type: result[1] || '', + name: result[2], + params: result[3] + .split(',') + .map((item) => item.trim()) + .filter((item) => Boolean(item)), + body: result[4] + } + } + return null +} + +// 解析JSX字符串为可执行函数 +const parseJSXFunction = (data, _scope, ctx) => { + try { + const newValue = transformJSX(data.value) + const fnInfo = parseFunctionString(newValue) + if (!fnInfo) throw Error('函数解析失败,请检查格式。示例:function fnName() { }') + + return newFn(...fnInfo.params, fnInfo.body).bind({ + ...ctx, + getComponent: ctx.getComponent + }) + } catch (error) { + Notify({ + type: 'warning', + title: '函数声明解析报错', + message: error?.message || '函数声明解析报错,请检查语法' + }) + + return newFn() + } +} + +export const generateFn = (innerFn, context) => { + return (...args) => { + // 如果有数据源标识,则表格的fetchData返回数据源的静态数据 + const sourceId = context?.collectionMethodsMap?.[innerFn.realName || innerFn.name] + if (sourceId) { + return innerFn.call(context, ...args) + } else { + let result = null + + // 这里是为了兼容用户写法报错导致画布异常,但无法捕获promise内部的异常 + try { + result = innerFn.call(context, ...args) + } catch (error) { + Notify({ + type: 'warning', + title: `函数:${innerFn.name}执行报错`, + message: error?.message || `函数:${innerFn.name}执行报错,请检查语法` + }) + } + + // 这里注意如果innerFn返回的是一个promise则需要捕获异常,重新返回默认一条空数据 + if (result?.then) { + result = new Promise((resolve) => { + result.then(resolve).catch((error) => { + Notify({ + type: 'warning', + title: '异步函数执行报错', + message: error?.message || '异步函数执行报错,请检查语法' + }) + // 这里需要至少返回一条空数据,方便用户使用表格默认插槽 + resolve({ + result: [{}], + page: { total: 1 } + }) + }) + }) + } + + return result + } + } +} + +const parseJSFunction = (data, _scope, ctx) => { + try { + const innerFn = newFn(`return ${data.value}`).bind(ctx)() + return generateFn(innerFn, ctx) + } catch (error) { + return parseJSXFunction(data, null, ctx) + } +} + +const parseJSSlot = (_data, _scope, _ctx) => { + return (_$scope) => { + // 这里需要导入 renderDefault,但由于循环依赖问题,暂时返回空函数 + // 实际使用时会在 render.ts 中处理 + return [] + } +} + +export function parseData(data, scope, ctx) { + const typeParser = parseList.find((item) => item.type(data)) + return typeParser ? typeParser.parseFunc(data, scope, ctx) : data +} + +export const parseCondition = (condition, scope, ctx) => { + // eslint-disable-next-line no-eq-null + return condition == null ? true : parseData(condition, scope, ctx) +} + +export const parseLoopArgs = (loop?: { item: unknown; index: number; loopArgs?: string[] }) => { + if (!loop) { + return undefined + } + const { item, index, loopArgs = [] } = loop + const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }` + return newFn('item,index', body)(item, index) +} + +const getIcon = (name) => window.TinyVueIcon?.[name]?.() || '' + +const parseIcon = (data, _scope, _ctx) => { + return getIcon(data.props.name) +} + +const parseStateAccessor = (data, _scope, ctx) => { + return parseData(data.defaultValue, null, ctx) +} + +const parseObjectData = (data, scope, ctx) => { + if (!data) { + return data + } + + // 如果是状态访问器,则直接解析默认值 + if (isStateAccessor(data)) { + return parseData(data.defaultValue, scope, ctx) + } + + // 解析通过属性传递icon图标组件 + if (data.componentName === 'Icon') { + return getIcon(data.props.name) + } + + const res = {} + Object.entries(data).forEach(([key, value]: [string, any]) => { + // 如果是插槽则需要进行特殊处理 + if (key === 'slot' && value?.name) { + res[key] = value.name + } else { + res[key] = parseData(value, scope, ctx) + } + }) + + // 处理 v-model 双向绑定 + const propsEntries = Object.entries(data) + const modelValue = propsEntries.find(([_key, value]) => value?.type === 'JSExpression' && value?.model === true) + const hasUpdateModelValue = propsEntries.find( + ([key]) => /^on[A-Z]/.test(key) && key.startsWith(`onUpdate:${modelValue?.[0]}`) + ) + + if (modelValue && !hasUpdateModelValue) { + // 添加 onUpdate:modelKey 事件 + res[`onUpdate:${modelValue?.[0]}`] = parseData( + { + type: 'JSFunction', + value: `(value) => ${modelValue[1].value}=value` + }, + scope, + ctx + ) + } + + return res +} + +const parseString = (data) => { + return data.trim() +} + +const parseArray = (data, scope, ctx) => { + return data.map((item) => parseData(item, scope, ctx)) +} + +const parseFunction = (data, scope, ctx) => { + return data.bind(ctx) +} + +parseList.push( + ...[ + { + type: isJSExpression, + parseFunc: parseExpression + }, + { + type: isI18nData, + parseFunc: parseI18n + }, + { + type: isJSFunction, + parseFunc: parseJSFunction + }, + { + type: isJSResource, + parseFunc: parseExpression + }, + { + type: isJSSlot, + parseFunc: parseJSSlot + }, + { + type: isIcon, + parseFunc: parseIcon + }, + { + type: isStateAccessor, + parseFunc: parseStateAccessor + }, + { + type: isString, + parseFunc: parseString + }, + { + type: isArray, + parseFunc: parseArray + }, + { + type: isFunction, + parseFunc: parseFunction + }, + { + type: isObject, + parseFunc: parseObjectData + } + ] +) diff --git a/packages/runtime-renderer/src/renderer/render.ts b/packages/runtime-renderer/src/renderer/render.ts new file mode 100644 index 0000000000..0a46927fc2 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/render.ts @@ -0,0 +1,384 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { h, provide, inject, defineComponent } from 'vue' +import { isHTMLTag, hyphenate } from '@vue/shared' +import TinyVue from '@opentiny/vue' +import { getBlockContext } from './page-function/blockContext' +import { + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasFlexBox, + CanvasSection +} from '@opentiny/tiny-engine-builtin-component' +import { ElInput, ElDatePicker, ElButton, ElForm, ElFormItem, ElTable, ElTableColumn } from 'element-plus' +import 'element-plus/dist/index.css' +import { + CanvasBox, + CanvasIcon, + CanvasText, + CanvasSlot, + CanvasImg, + CanvasPlaceholder, + CanvasRouterLink, + CanvasRouterView, + CanvasCollection +} from './builtin' +import { parseData, parseCondition, parseLoopArgs } from './parser' + +const hyphenateRE = /\B([A-Z])/g +const customElements = {} + +const Mapper = { + Icon: CanvasIcon, + Text: CanvasText, + div: CanvasBox, + Slot: CanvasSlot, + slot: CanvasSlot, + Template: CanvasBox, + Img: CanvasImg, + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasFlexBox, + CanvasSection, + CanvasPlaceholder, + RouterLink: CanvasRouterLink, + RouterView: CanvasRouterView, + Collection: CanvasCollection, + ElInput, + ElDatePicker, + ElButton, + ElForm, + ElFormItem, + ElTable, + ElTableColumn +} + +export const collectionMethodsMap = {} + +const getNative = (name) => { + return TinyVue?.[name] +} + +const getBlock = (name) => { + return window.blocks?.[name] +} + +export const getComponent = (name) => { + // 首先尝试从映射表、原生组件、自定义元素中获取 + const component = Mapper[name] || getNative(name) || customElements[name] + if (component) { + return component + } + + // 如果是 HTML 标签,直接返回 + if (isHTMLTag(name)) { + return name + } + + // 检查是否是区块组件 + const blockSchema = getBlock(name) + if (blockSchema) { + // 返回一个动态组件,用于渲染区块 + return defineComponent({ + name: `${name}`, + setup() { + // 区块的真实内容在 window.blocks 中,而不是页面的 schema 中 + // 页面的 schema 只是区块的引用,children 为空 + const blockContent = blockSchema.schema + + const context = getBlockContext(blockContent) + + return { + context + } + }, + render() { + // 递归渲染区块的 children + const blockContent = blockSchema.schema + const context = this.context + + // eslint-disable-next-line + return renderGroup(blockContent.children, {}, context, renderComponent) + } + }) + } + + return CanvasPlaceholder +} + +const configure = {} + +export const setConfigure = (configureData) => { + Object.assign(configure, configureData) +} + +const _getPlainProps = (object = {}) => { + const { slot, ...rest } = object + const props = {} + + if (slot) { + rest.slot = slot.name || slot + } + + Object.entries(rest).forEach(([key, value]) => { + let renderKey = key + + // html 标签属性会忽略大小写,所以传递包含大写的 props 需要转换为 kebab 形式的 props + if (!/on[A-Z]/.test(renderKey) && hyphenateRE.test(renderKey)) { + renderKey = hyphenate(renderKey) + } + + if (['boolean', 'string', 'number'].includes(typeof value)) { + props[renderKey] = value + } else { + // 如果传给webcomponent标签的是对象或者数组需要使用.prop修饰符,转化成h函数就是如下写法 + props[`.${renderKey}`] = value + } + }) + return props +} + +const generateCollection = (schema) => { + if (schema.componentName === 'Collection' && schema.props?.dataSource && schema.children) { + schema.children.forEach((item) => { + const fetchData = item.props?.fetchData + const methodMatch = fetchData?.value?.match(/this\.(.+?)}/) + if (fetchData && methodMatch?.[1]) { + const methodName = methodMatch[1].trim() + // 缓存表格fetchData对应的数据源信息 + collectionMethodsMap[methodName] = schema.props.dataSource + } + }) + } +} + +const renderDefault = ( + children: any[], + scope: Record, + parent: any, + renderComponent: (schema: any, scope: Record, parent: any) => any +) => children.map?.((child) => renderComponent(child, scope, parent)) + +const generateSlotGroup = (children, isCustomElm, schema) => { + const slotGroup = {} + + children.forEach((child) => { + const { componentName, children, params = [], props } = child + const slot = child.slot || props?.slot?.name || props?.slot || 'default' + const isNotEmptyTemplate = componentName === 'Template' && children.length + + if (isCustomElm) { + child.props.slot = 'slot' // CE下需要给子节点加上slot标识 + } + slotGroup[slot] = slotGroup[slot] || { + value: [], + params, + parent: isNotEmptyTemplate ? child : schema + } + + slotGroup[slot].value.push(...(isNotEmptyTemplate ? children : [child])) // template 标签直接过滤掉 + }) + + return slotGroup +} + +const renderSlot = (children, scope, schema, isCustomElm, context, renderComponent) => { + if (children.some((a) => a.componentName === 'Template')) { + const slotGroup = generateSlotGroup(children, isCustomElm, schema) + const slots = {} + + Object.keys(slotGroup).forEach((slotName) => { + const currentSlot = slotGroup[slotName] + + slots[slotName] = ($scope) => renderDefault(currentSlot.value, { ...scope, ...$scope }, context, renderComponent) + }) + + return slots + } + + return { default: () => renderDefault(children, scope, context, renderComponent) } +} + +const _checkGroup = (componentName) => configure[componentName]?.nestingRule?.childWhitelist?.length + +const directChildrenHasTemplate = (children) => children.some((child) => child.componentName === 'Template') + +const getBindProps = (schema, scope, context) => { + const { componentName } = schema + + if (componentName === 'CanvasPlaceholder') { + return {} + } + + const bindProps = { + ...parseData(schema.props, scope, context) + } + + if (Mapper[componentName]) { + bindProps.schema = schema + } + + // 如果是区块组件,传递完整的 schema + const blockSchema = getBlock(componentName) + if (blockSchema) { + bindProps.schema = schema + } + + // 绑定组件属性时需要将 className 重命名为 class,防止覆盖组件内置 class + bindProps.class = bindProps.className + delete bindProps.className + + return bindProps +} + +const getLoopScope = ({ scope, index, item, loopArgs }) => { + return { + ...scope, + ...(parseLoopArgs({ + item, + index, + loopArgs + }) || {}) + } +} + +const injectPlaceHolder = (componentName, children) => { + const isEmptyArr = Array.isArray(children) && !children.length + + if (configure[componentName]?.isContainer && (!children || isEmptyArr)) { + return [ + { + componentName: 'CanvasPlaceholder' + } + ] + } + + return children +} + +const renderGroup = (children, scope, context, renderComponent) => { + return children.map?.((schema) => { + const { componentName, children, loop, loopArgs, condition } = schema + const loopList = parseData(loop, scope, context) + + const renderElement = (item, index) => { + const mergeScope = getLoopScope({ + scope, + index, + item, + loopArgs + }) + + if (!parseCondition(condition, mergeScope, context)) { + return null + } + + const renderChildren = injectPlaceHolder(componentName, children) + + const element = h( + getComponent(componentName), + getBindProps(schema, mergeScope, context), + Array.isArray(renderChildren) + ? renderSlot(renderChildren, mergeScope, schema, customElements[componentName], context, renderComponent) + : parseData(renderChildren, mergeScope, context) + ) + + return element + } + + return loopList?.length ? loopList.map(renderElement) : renderElement(undefined, 0) + }) +} + +const getChildren = (schema, mergeScope, context, renderComponent) => { + const { componentName, children } = schema + const renderChildren = injectPlaceHolder(componentName, children) + + if (!Array.isArray(renderChildren)) { + return parseData(renderChildren, mergeScope, context) + } + + if (!renderChildren.length) { + return null + } + + const isCustomElm = customElements[componentName] + + if (directChildrenHasTemplate(renderChildren)) { + return renderSlot(renderChildren, mergeScope, schema, isCustomElm, context, renderComponent) + } + + return renderGroup(renderChildren, mergeScope, context, renderComponent) +} + +function renderComponent(schema, scope, parent) { + const { componentName, loop, loopArgs, condition } = schema + + // 处理数据源和表格fetchData的映射关系 + generateCollection(schema) + + if (!componentName) { + return parseData(schema, scope, parent) + } + + const component = getComponent(componentName) + + const loopList = parseData(loop, scope, parent) + + const renderElement = (item, index) => { + const mergeScope = item + ? getLoopScope({ + item, + index, + loopArgs, + scope + }) + : scope + + if (!parseCondition(condition, mergeScope, parent)) { + return null + } + + const Ele = h( + component, + getBindProps(schema, mergeScope, parent), + getChildren(schema, mergeScope, parent, renderComponent) + ) + + return Ele + } + + return loopList?.length ? loopList.map(renderElement) : renderElement(undefined, 0) +} + +export const renderer = defineComponent({ + name: 'renderer', + props: { + schema: Object, + scope: Object, + parent: Object + }, + setup(props) { + provide('schema', props.schema) + }, + render() { + const context = inject('pageContext') + const { scope, schema } = this + + return renderComponent(schema, scope, context, renderComponent) + } +}) + +export default renderer diff --git a/packages/runtime-renderer/src/renderer/useContext.ts b/packages/runtime-renderer/src/renderer/useContext.ts new file mode 100644 index 0000000000..20f7ce9e8c --- /dev/null +++ b/packages/runtime-renderer/src/renderer/useContext.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { shallowReactive, type ShallowReactive } from 'vue' + +interface Context { + [key: string]: any +} + +interface UseContextReturn { + context: ShallowReactive + setContext: (ctx: Context, clear?: boolean) => void + getContext: () => ShallowReactive +} + +export default (): UseContextReturn => { + const context = shallowReactive({}) + + // 从大纲树控制隐藏 + + const setContext = (ctx: Context, clear?: boolean) => { + if (clear) { + Object.keys(context).forEach((key) => delete context[key]) + } + Object.assign(context, ctx) + } + + const getContext = () => context + + return { + context, + setContext, + getContext + } +} From e35bd65ccc22157b6712bd4cb5429ec78e326606 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 09:19:35 +0800 Subject: [PATCH 08/81] feat: Part of lifecycle support There are some bugs in lifecycle execution(onMount, onBeforeUpdate, onUpdated, onErrorCaptured, onUnmounted) --- .../src/renderer/LifecycleWrapper.ts | 208 ++++++++++++++++++ .../src/renderer/RenderMain.ts | 4 +- 2 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 packages/runtime-renderer/src/renderer/LifecycleWrapper.ts diff --git a/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts b/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts new file mode 100644 index 0000000000..3e51a352f7 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts @@ -0,0 +1,208 @@ +import { + defineComponent, + inject, + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, + onDeactivated, + watchEffect, + ref, + h, + type PropType +} from 'vue' +import { Notify } from '@opentiny/vue' +import renderer from './render' +import { parseData } from './parser' + +interface JSFunction { + type: 'JSFunction' + value: string +} + +// 执行用户定义的生命周期函数 +const executeUserLifecycle = (hookName: string, lifeCycleConfig: JSFunction | undefined, context: any) => { + if (!lifeCycleConfig || lifeCycleConfig.type !== 'JSFunction') { + return + } + + try { + const fn = parseData(lifeCycleConfig, {}, context) + if (typeof fn === 'function') { + fn.call(context, context) + } + } catch (error) { + Notify({ + type: 'warning', + title: `${hookName} 生命周期执行失败`, + message: (error as any)?.message || `${hookName} 生命周期函数执行报错,请检查语法` + }) + } +} + +// 页面级生命周期包裹器 +export const PageLifecycleWrapper = defineComponent({ + name: 'PageLifecycleWrapper', + props: { + schema: { + type: Object as PropType, + required: true + }, + parent: { + type: Object, + default: () => ({}) + } + }, + setup(props) { + const lifeCycles = props.parent.lifeCycles + const pageContext = inject('pageContext') as any + let isInitialized = false + + // setup 生命周期 - 在组件创建时立即执行 + if (lifeCycles?.setup) { + executeUserLifecycle('setup', lifeCycles?.setup, pageContext) + } + + // 创建响应式状态,用于监听变化 + const reactiveState = ref({ + schema: props.schema, + timestamp: Date.now(), + // 添加页面状态变化的追踪 + stateSnapshot: null as any + }) + + // 监听页面状态变化,触发更新生命周期 + watchEffect(() => { + if (!isInitialized) { + return + } + + // 获取页面上下文的最新状态 + const pageContextData = pageContext.getContext() + + // 监听页面上下文中的响应式数据 + const { state, stores } = pageContextData + + // 监听状态变化, 建立响应式依赖 + if (state) { + // 访问 state 的各个属性,建立响应式依赖 + Object.keys(state).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + state[key] // 访问会建立响应式依赖 + }) + } + + // 监听 stores 变化,建立响应式依赖 + if (stores) { + Object.keys(stores).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + stores[key] // 这里访问会建立响应式依赖 + }) + } + + // 监听数据源变化 - 建立响应式依赖 + const dataSourceMap = pageContextData.dataSourceMap + if (dataSourceMap) { + Object.keys(dataSourceMap).forEach((key) => { + const dataSource = dataSourceMap[key] + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + dataSource // 这里访问会建立响应式依赖 + }) + } + + // 创建状态快照,用于追踪变化 + const stateSnapshot = { + state: state ? JSON.stringify(state) : null, + stores: stores ? JSON.stringify(stores) : null, + dataSourceMap: dataSourceMap ? JSON.stringify(dataSourceMap) : null + } + + // 更新响应式状态,这会触发组件重新渲染 + const newTimestamp = Date.now() + reactiveState.value = { + schema: props.schema, + timestamp: newTimestamp, + stateSnapshot + } + }) + + // onBeforeMount 生命周期 + onBeforeMount(() => { + executeUserLifecycle('onBeforeMount', lifeCycles?.onBeforeMount, pageContext) + }) + + // onMounted 生命周期 + onMounted(() => { + executeUserLifecycle('onMounted', lifeCycles?.onMounted, pageContext) + isInitialized = true // 标记为已初始化,允许更新生命周期执行 + }) + + // onBeforeUpdate 生命周期 + onBeforeUpdate(() => { + if (isInitialized) { + executeUserLifecycle('onBeforeUpdate', lifeCycles?.onBeforeUpdate, pageContext) + } + }) + + // onUpdated 生命周期 + onUpdated(() => { + if (isInitialized) { + executeUserLifecycle('onUpdated', lifeCycles?.onUpdated, pageContext) + } + }) + + // onBeforeUnmount 生命周期 + onBeforeUnmount(() => { + executeUserLifecycle('onBeforeUnmount', lifeCycles?.onBeforeUnmount, pageContext) + isInitialized = false // 重置初始化状态 + }) + + // onUnmounted 生命周期 + onUnmounted(() => { + executeUserLifecycle('onUnmounted', lifeCycles?.onUnmounted, pageContext) + }) + + // onErrorCaptured 生命周期 + onErrorCaptured((error, instance, info) => { + if (lifeCycles?.onErrorCaptured) { + try { + const fn = parseData(lifeCycles?.onErrorCaptured, {}, pageContext) + if (typeof fn === 'function') { + // 将错误信息传递给用户函数 + const result = fn.call(pageContext, error, instance, info) + // 如果用户函数返回false,阻止错误继续传播 + return result === false + } + } catch (userError) { + Notify({ + type: 'warning', + title: 'onErrorCaptured 生命周期执行失败', + message: (userError as any)?.message || 'onErrorCaptured 生命周期函数执行报错,请检查语法' + }) + } + } + // 默认让错误继续传播 + return true + }) + + // onActivated 生命周期 (keep-alive 组件激活时) + onActivated(() => { + executeUserLifecycle('onActivated', lifeCycles?.onActivated, pageContext) + }) + + // onDeactivated 生命周期 (keep-alive 组件失活时) + onDeactivated(() => { + executeUserLifecycle('onDeactivated', lifeCycles?.onDeactivated, pageContext) + }) + + return () => + h(renderer, { + schema: reactiveState.value.schema, + parent: props.parent + }) + } +}) diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 7a81d497b5..881d100031 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -13,7 +13,7 @@ import { h, computed, provide, nextTick, reactive, watch, defineComponent } from 'vue' import Loading from '../components/Loading.vue' import { parseData } from './parser/parser.ts' -import { renderer } from './render.ts' +import { PageLifecycleWrapper } from './LifecycleWrapper.ts' import { setPageCss } from './page-function/css-handler.ts' import { useState } from './page-function/state' import useContext from './useContext.ts' @@ -116,7 +116,7 @@ export default defineComponent({ } return this.pageSchema.children?.length - ? h(renderer, { schema: rootChildrenSchema, parent: this.pageSchema }) + ? h(PageLifecycleWrapper, { schema: rootChildrenSchema, parent: this.pageSchema }) : [h(Loading)] } }) From 7b73cea706d2e825734ff25559b0cacedbbd69c3 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 09:28:58 +0800 Subject: [PATCH 09/81] feat: Add vue-router based routing capability for application schema Support homepage setting, nested routes, and default navigation to subpages for nested routes --- packages/runtime-renderer/index.ts | 12 +- packages/runtime-renderer/src/App.vue | 2 +- .../src/components/NotFound.vue | 210 ++++++++++++++++++ .../src/components/PageRenderer.vue | 9 + .../src/composables/useAppSchema.ts | 139 ++++++++++++ .../src/renderer/RenderMain.ts | 7 +- packages/runtime-renderer/src/router/index.ts | 91 ++++++++ 7 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 packages/runtime-renderer/src/components/NotFound.vue create mode 100644 packages/runtime-renderer/src/components/PageRenderer.vue create mode 100644 packages/runtime-renderer/src/composables/useAppSchema.ts create mode 100644 packages/runtime-renderer/src/router/index.ts diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index 89c0ad73a0..11b66eeb6e 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -11,11 +11,21 @@ */ import { createApp } from 'vue' +import { useAppSchema } from './src/composables/useAppSchema' +import { createAppRouter } from './src/router' import App from './src/App.vue' // 初始化运行时渲染器 export const initRuntimeRenderer = async () => { + const searchParams = new URLSearchParams(location.search) + const appId = searchParams.get('id') + const { fetchAppSchema, fetchBlocks } = useAppSchema() + await fetchAppSchema(appId || '') + await fetchBlocks() + const router = await createAppRouter() + const app = createApp(App) - app.mount('#app') + app.use(router).mount('#app') + return app } diff --git a/packages/runtime-renderer/src/App.vue b/packages/runtime-renderer/src/App.vue index 7b8b46cb04..ee8caf2c46 100644 --- a/packages/runtime-renderer/src/App.vue +++ b/packages/runtime-renderer/src/App.vue @@ -1,3 +1,3 @@ diff --git a/packages/runtime-renderer/src/components/NotFound.vue b/packages/runtime-renderer/src/components/NotFound.vue new file mode 100644 index 0000000000..62af36799d --- /dev/null +++ b/packages/runtime-renderer/src/components/NotFound.vue @@ -0,0 +1,210 @@ + + + + + + diff --git a/packages/runtime-renderer/src/components/PageRenderer.vue b/packages/runtime-renderer/src/components/PageRenderer.vue new file mode 100644 index 0000000000..fb65f3118a --- /dev/null +++ b/packages/runtime-renderer/src/components/PageRenderer.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts new file mode 100644 index 0000000000..321487115a --- /dev/null +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -0,0 +1,139 @@ +import { ref, computed, readonly } from 'vue' +import type { AppSchema, BlockItem, BlockContent } from '../types/schema' + +const appSchema = ref(null) +const isLoading = ref(false) +const error = ref(null) +export function useAppSchema() { + // 注入全局CSS + const injectGlobalCSS = (css: string) => { + if (!css) return + + const style = document.createElement('style') + style.textContent = css + document.head.appendChild(style) + } + + // 初始化应用配置 + const initializeAppConfig = async (schema: AppSchema) => { + if (!schema?.data) return + + // 注入全局CSS + injectGlobalCSS(schema.data.css) + } + + // 拉取完整应用schema + const fetchAppSchema = async (appId: string) => { + isLoading.value = true + error.value = null + + try { + const response = await fetch(`/app-center/v1/api/apps/schema/${appId}`) + appSchema.value = await response.json() + + // 解析并初始化应用级配置 + await initializeAppConfig(appSchema.value) + } catch (err) { + error.value = err instanceof Error ? err.message : '加载应用Schema失败' + // eslint-disable-next-line no-console + console.error('加载应用Schema失败:', err) + } finally { + isLoading.value = false + } + } + + // 拉取区块schema + const fetchBlocks = async () => { + const response = await fetch('/material-center/api/blocks') + const blockJSON = await response.json() + const blocks: BlockItem[] = blockJSON.data || [] + + // 转换为组件映射格式 + const blocksMap: Record< + string, + { + schema: BlockContent + meta: { + id: number + label: string + framework: string + version: string + } + } + > = {} + blocks.forEach((block) => { + if (block.content) { + blocksMap[block.label] = { + schema: block.content, + meta: { + id: block.id, + label: block.label, + framework: block.framework, + version: block.version + } + } + } + }) + + window.blocks = blocksMap + } + + // 获取页面列表 + const pages = computed(() => { + if (!appSchema.value?.data?.componentsTree) return [] + return appSchema.value.data.componentsTree + }) + // 根据ID获取页面 + const getPageById = (id: number) => { + if (!pages.value) return null + return pages.value.find((page) => page.meta.id === id) + } + + // 获取数据源配置 + const dataSourceConfig = computed(() => { + return appSchema.value?.data?.dataSource || {} + }) + + // 获取全局状态配置 + const globalStates = computed(() => { + return appSchema.value?.data?.meta?.globalState || [] + }) + + // 获取包依赖 + const packages = computed(() => { + return appSchema.value?.data?.packages || [] + }) + + // 检查应用是否已加载 + const isAppLoaded = computed(() => { + return !!appSchema.value + }) + + const i18nConfig = computed(() => { + return appSchema.value?.data?.i18n || {} + }) + + return { + // 状态 + appSchema: readonly(appSchema), + isLoading: readonly(isLoading), + error: readonly(error), + + // 计算属性 + pages, + dataSourceConfig, + globalStates, + packages, + isAppLoaded, + i18nConfig, + + // 方法 + fetchAppSchema, + fetchBlocks, + getPageById, + + // 初始化方法 + initializeAppConfig, + injectGlobalCSS + } +} diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 881d100031..3c0c3f63ab 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -17,6 +17,7 @@ import { PageLifecycleWrapper } from './LifecycleWrapper.ts' import { setPageCss } from './page-function/css-handler.ts' import { useState } from './page-function/state' import useContext from './useContext.ts' +import { useRouter, useRoute } from 'vue-router' import { useAppSchema } from '../composables/useAppSchema' import type { PageContent as Schema } from '../types/schema' @@ -40,6 +41,8 @@ export default defineComponent({ return JSON.parse(JSON.stringify(page)) }) + const route = useRoute() + const router = useRouter() const { context, setContext, getContext } = useContext() const reset = (obj: Record) => { Object.keys(obj).forEach((key) => delete obj[key]) @@ -72,7 +75,9 @@ export default defineComponent({ const newSchema = JSON.parse(JSON.stringify(data)) const context = { - state + state, + route, + router } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 setContext(context, true) diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/router/index.ts new file mode 100644 index 0000000000..0704d30858 --- /dev/null +++ b/packages/runtime-renderer/src/router/index.ts @@ -0,0 +1,91 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import { useAppSchema } from '../composables/useAppSchema' +import type { RouteConfig } from '../types/config' +import { reactive } from 'vue' +// 异步初始化路由配置 +async function createRouterConfig() { + const { pages } = useAppSchema() + + // 生成路由配置 + const generateRoutesConfig = () => { + if (!pages.value) return [] + + const routesConfig = reactive([]) + + // 遍历页面列表生成路由配置 + pages.value.forEach((page) => { + const isChildRoute = page.meta.parentId !== '0' + + const routeConfigCurrent = { + path: isChildRoute ? page.meta.router : `/${page.meta.router}`, + name: `${page.meta.id}`, + component: () => import('../components/PageRenderer.vue'), // 懒加载,避免过早引入RenderMain + props: { pageId: page.meta.id }, // 静态对象,避免路由嵌套时被覆盖 + children: [], + meta: { + pageId: page.meta.id, + pageName: page.meta.name, + isHome: page.meta.isHome, + hasChildren: (page.children && page.children.length > 0) || false, + depth: page.meta.depth, // 疑问:在嵌套路由中此属性没有改变,此属性和面包屑有关吗? + isDefault: page.meta.isDefault, // 用于嵌套路由的默认子路由 + hasDefault: false, + defaultPath: '', // 默认子路由的路径 + parentPath: '/' + } + } + + if (isChildRoute) { + const parentId = parseInt(page.meta.parentId) + const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId) + if (parentRoute) { + parentRoute.children = parentRoute.children || [] + parentRoute.children.push(routeConfigCurrent) + parentRoute.meta.hasChildren = true + if (routeConfigCurrent.meta.isDefault) { + parentRoute.meta.hasDefault = true + parentRoute.meta.defaultPath = `${parentRoute.path}/${routeConfigCurrent.path}` + parentRoute.redirect = parentRoute.meta.defaultPath + } + return + } + } else { + routesConfig.push(routeConfigCurrent) + } + }) + + return routesConfig + } + + const routes: any[] = [] + const routesConfig = generateRoutesConfig() + + routesConfig.forEach((page) => { + routes.push(page) + if (page.meta.isHome) { + routes.push({ path: '/', redirect: `${page.path}` }) + } + }) + + routes.push({ + path: '/:pathMatch(.*)*', + component: () => import('../components/NotFound.vue') + }) + + return routes +} + +export async function createAppRouter() { + const routes = await createRouterConfig() + const router = createRouter({ history: createWebHashHistory('/runtime.html'), routes }) + + if (typeof window !== 'undefined') { + window.__DEBUG_ROUTER__ = router + // eslint-disable-next-line no-console + console.log( + '所有路由:', + router.getRoutes().map((r) => ({ path: r.path, name: r.name, redirect: r.redirect })) + ) + } + return router +} From fe14595a1842c6852a1340f2b98f656fc61eafc5 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 10:24:28 +0800 Subject: [PATCH 10/81] feat: Add Pinia-based stores capability --- packages/runtime-renderer/index.ts | 9 +++- .../src/renderer/RenderMain.ts | 6 ++- packages/runtime-renderer/src/stores/index.ts | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 packages/runtime-renderer/src/stores/index.ts diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index 11b66eeb6e..c20b83dbf9 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -13,6 +13,8 @@ import { createApp } from 'vue' import { useAppSchema } from './src/composables/useAppSchema' import { createAppRouter } from './src/router' +import { createPinia } from 'pinia' +import { createStores, generateStoresConfig } from './src/stores' import App from './src/App.vue' // 初始化运行时渲染器 @@ -24,8 +26,13 @@ export const initRuntimeRenderer = async () => { await fetchBlocks() const router = await createAppRouter() + const pinia = createPinia() + const storesConfig = generateStoresConfig() + const stores = createStores(storesConfig, pinia) + const app = createApp(App) - app.use(router).mount('#app') + app.use(pinia).use(router).mount('#app') + app.provide('stores', stores) return app } diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 3c0c3f63ab..39061012cd 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -10,7 +10,7 @@ * */ -import { h, computed, provide, nextTick, reactive, watch, defineComponent } from 'vue' +import { h, computed, provide, nextTick, reactive, watch, defineComponent, inject } from 'vue' import Loading from '../components/Loading.vue' import { parseData } from './parser/parser.ts' import { PageLifecycleWrapper } from './LifecycleWrapper.ts' @@ -47,6 +47,7 @@ export default defineComponent({ const reset = (obj: Record) => { Object.keys(obj).forEach((key) => delete obj[key]) } + const stores = inject('stores') provide('pageContext', context) const pageSchema = reactive({} as Schema) @@ -77,7 +78,8 @@ export default defineComponent({ const context = { state, route, - router + router, + stores } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 setContext(context, true) diff --git a/packages/runtime-renderer/src/stores/index.ts b/packages/runtime-renderer/src/stores/index.ts new file mode 100644 index 0000000000..f1a7609844 --- /dev/null +++ b/packages/runtime-renderer/src/stores/index.ts @@ -0,0 +1,45 @@ +import { defineStore, type Pinia } from 'pinia' +import { shallowReactive } from 'vue' +import type { StoreConfig } from '../types/config' +import { useAppSchema } from '../composables/useAppSchema' +import { parseJSFunction } from '../utils/data-utils' + +export const generateStoresConfig = () => { + const { globalStates } = useAppSchema() + if (globalStates.value.length === 0) return [] + return globalStates.value.map((store) => ({ + id: store.id, + state: JSON.parse(JSON.stringify(store.state)), + actions: Object.fromEntries( + Object.keys(store.actions || {}).map((key) => { + // 使用 parseJSFunction ,但是上下文由pinia内部绑定 + return [key, parseJSFunction(store.actions[key], {}, {})] + }) + ), + getters: Object.fromEntries( + Object.keys(store.getters || {}).map((key) => { + // 同样处理 getters + return [key, parseJSFunction(store.getters[key], {}, {})] + }) + ) + })) +} + +export const createStores = (storesConfig: StoreConfig[], pinia: Pinia) => { + const stores = shallowReactive>({}) + + storesConfig.forEach((config) => { + // 使用 defineStore 创建 Pinia store + const useStore = defineStore(config.id, { + state: () => config.state, + + getters: config.getters, + + actions: config.actions + }) + // 使用useStore创建 store 实例并绑定到 pinia + stores[config.id] = useStore(pinia) + }) + + return stores +} From 44ffc601030689d632478ee9fe9aea082b60ffba Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 10:31:10 +0800 Subject: [PATCH 11/81] feat: Add data source capability Mainly based on the vue-generator code. Currently in a mixed state of JavaScript and TypeScript, which will be improved later. --- .../src/app-function/dataSource.ts | 121 +++++++++++++++ .../src/app-function/http/axios.js | 143 ++++++++++++++++++ .../src/app-function/http/config.js | 15 ++ .../src/app-function/http/index.js | 27 ++++ .../src/renderer/RenderMain.ts | 4 +- 5 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 packages/runtime-renderer/src/app-function/dataSource.ts create mode 100644 packages/runtime-renderer/src/app-function/http/axios.js create mode 100644 packages/runtime-renderer/src/app-function/http/config.js create mode 100644 packages/runtime-renderer/src/app-function/http/index.js diff --git a/packages/runtime-renderer/src/app-function/dataSource.ts b/packages/runtime-renderer/src/app-function/dataSource.ts new file mode 100644 index 0000000000..247bcc1bf9 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/dataSource.ts @@ -0,0 +1,121 @@ +import useHttp from './http' +import { useAppSchema } from '../composables/useAppSchema' +import { parseJSFunction } from '../utils/data-utils' + +const { dataSourceConfig } = useAppSchema() + +// 深拷贝防止修改原始 reactive +const rawConfig = JSON.parse(JSON.stringify(dataSourceConfig.value || {})) + +// 将原本的配置格式标准化以方便复用出码逻辑 +const normalizeItem = (item: any) => { + return { + id: item.id, + name: item.name, + columns: item.data.columns, + data: item.data.data, + type: item.data.type, + options: item.data.options, + dataHandler: item.data.dataHandler, + willFetch: item.data.willFetch, + shouldFetch: item.data.shouldFetch, + errorHandler: item.data.errorHandler + } +} + +const dataSources = { + dataHandler: rawConfig.dataHandler, + list: (rawConfig.list || []).map(normalizeItem) +} + +export const dataSourceMap: Record = {} + +const globalDataHandle = dataSources.dataHandler ? parseJSFunction(dataSources.dataHandler) : (res) => res + +// 统一的 load 构造 +const load = (http, options, dataSource, shouldFetch) => (params?, customUrl?) => { + // 无 options 视为本地/静态数据 + if (!options) { + try { + const raw = globalDataHandle(dataSource.config.data) + const items = Array.isArray(raw) ? raw : raw ? [raw] : [] + const wrapped = { code: '', msg: 'success', data: { items, total: items.length } } + dataSource.status = 'loaded' + dataSource.data = wrapped + return Promise.resolve(wrapped) + } catch (e) { + dataSource.status = 'error' + dataSource.error = e + return Promise.reject(e) + } + } + + if (!shouldFetch()) { + return Promise.resolve(undefined) + } + + dataSource.status = 'loading' + const { method = 'GET', uri: url, params: defaultParams, timeout, headers } = options + const config: any = { method, url, headers, timeout } + + const data = params || defaultParams + config.url = customUrl || config.url + + if (method.toLowerCase() === 'get') { + config.params = data + } else { + config.data = data + } + + return http.request(config) +} + +// 构建每个数据源 +dataSources.list.forEach((config) => { + const http = useHttp(globalDataHandle) + const dataSource = { + config: config, + status: 'init', + data: { data: config.data } // 保持占位,后续 remote 成功后再写 + } + + dataSourceMap[config.name] = dataSource + + const shouldFetch = config.shouldFetch?.value ? parseJSFunction(config.shouldFetch) : () => true + const willFetch = config.willFetch?.value ? parseJSFunction(config.willFetch) : (options) => options + + const dataHandler = (res) => { + const handled = config.dataHandler?.value ? parseJSFunction(config.dataHandler)(res) : res + dataSource.status = 'loaded' + dataSource.data = handled + return handled + } + + const errorHandler = (error) => { + if (config.errorHandler?.value) { + parseJSFunction(config.errorHandler)(error) + } + dataSource.status = 'error' + dataSource.error = error + return Promise.reject(error) + } + + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + + if (import.meta.env.VITE_APP_MOCK === 'mock') { + http.mock([ + { + url: config.options?.uri, + response() { + return Promise.resolve([200, { data: config.data }]) + } + }, + { url: '*', proxy: '*' } + ]) + } + + dataSource.load = load(http, config.options, dataSource, shouldFetch) +}) + +export default dataSourceMap diff --git a/packages/runtime-renderer/src/app-function/http/axios.js b/packages/runtime-renderer/src/app-function/http/axios.js new file mode 100644 index 0000000000..3654c619b4 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/http/axios.js @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +export default (config) => { + const instance = axios.create(config) + const defaults = {} + let mock + + if (typeof MockAdapter.prototype.proxy === 'undefined') { + MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let stream = this + const request = (proxy, any) => { + return (setting) => { + return new Promise((resolve) => { + config.responseType = 'json' + axios + .get(any ? proxy + setting.url + '.json' : proxy, config) + .then(({ data }) => { + if (typeof handleData === 'function') { + data = handleData.call(null, data, setting) + } + resolve([200, data]) + }) + .catch((error) => { + resolve([error.response.status, error.response.data]) + }) + }) + } + } + + if (url === '*' && proxy && typeof proxy === 'string') { + stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true)) + } else { + if (proxy && typeof proxy === 'string') { + stream = this.onAny(url).reply(request(proxy)) + } else if (typeof response === 'function') { + stream = this.onAny(url).reply(response) + } + } + + return stream + } + } + + return { + request(config) { + return instance(config) + }, + get(url, config) { + return instance.get(url, config) + }, + delete(url, config) { + return instance.delete(url, config) + }, + head(url, config) { + return instance.head(url, config) + }, + post(url, data, config) { + return instance.post(url, data, config) + }, + put(url, data, config) { + return instance.put(url, data, config) + }, + patch(url, data, config) { + return instance.patch(url, data, config) + }, + all(iterable) { + return axios.all(iterable) + }, + spread(callback) { + return axios.spread(callback) + }, + defaults(key, value) { + if (key && typeof key === 'string') { + if (typeof value === 'undefined') { + return instance.defaults[key] + } + instance.defaults[key] = value + defaults[key] = value + } else { + return instance.defaults + } + }, + defaultSettings() { + return defaults + }, + interceptors: { + request: { + use(fnHandle, fnError) { + return instance.interceptors.request.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.request.eject(id) + } + }, + response: { + use(fnHandle, fnError) { + return instance.interceptors.response.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.response.eject(id) + } + } + }, + mock(config) { + if (!mock) { + mock = new MockAdapter(instance) + } + + if (Array.isArray(config)) { + config.forEach((item) => { + mock.proxy(item) + }) + } + + return mock + }, + disableMock() { + if (mock) { + mock.restore() + } + mock = undefined + }, + isMock() { + return typeof mock !== 'undefined' + }, + CancelToken: axios.CancelToken, + isCancel: axios.isCancel + } +} diff --git a/packages/runtime-renderer/src/app-function/http/config.js b/packages/runtime-renderer/src/app-function/http/config.js new file mode 100644 index 0000000000..cfa3714e17 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/http/config.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +export default { + withCredentials: false +} diff --git a/packages/runtime-renderer/src/app-function/http/index.js b/packages/runtime-renderer/src/app-function/http/index.js new file mode 100644 index 0000000000..b0a08546a6 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/http/index.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import axios from './axios' +import config from './config' + +export default (dataHandler) => { + const http = axios(config) + + http.interceptors.response.use(dataHandler, (error) => { + const response = error.response + if (response.status === 403 && response.headers && response.headers['x-login-url']) { + // TODO 处理无权限时,重新登录再发送请求 + } + }) + + return http +} diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 39061012cd..9601552d4c 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -20,6 +20,7 @@ import useContext from './useContext.ts' import { useRouter, useRoute } from 'vue-router' import { useAppSchema } from '../composables/useAppSchema' import type { PageContent as Schema } from '../types/schema' +import dataSourceMap from '../app-function/dataSource.ts' interface Props { pageId: number @@ -79,7 +80,8 @@ export default defineComponent({ state, route, router, - stores + stores, + dataSourceMap } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 setContext(context, true) From 0b65946438670b41c99bd93004dfdc9f7f67270d Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 10:36:24 +0800 Subject: [PATCH 12/81] feat: Support using user-added utils in applications Support using destructured npm package imports --- .../src/app-function/utils.ts | 70 +++++++++++++++++++ .../src/composables/useAppSchema.ts | 16 ++++- .../src/renderer/RenderMain.ts | 4 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/runtime-renderer/src/app-function/utils.ts diff --git a/packages/runtime-renderer/src/app-function/utils.ts b/packages/runtime-renderer/src/app-function/utils.ts new file mode 100644 index 0000000000..b6e8281da9 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/utils.ts @@ -0,0 +1,70 @@ +import type { Util } from '../types/schema' +import { parseJSFunction } from '../utils/data-utils' + +interface npmContent { + package?: string + version?: string + exportName?: string + subName?: string + destructuring?: boolean + cdnLink?: string +} + +interface fnContent { + type: string + value: string +} + +const npmCache = new Map() +const utilValues = new Map() +let initialized = false +let loading = false + +async function loadNpmUtil(util: Util) { + const c = util.content as npmContent + if (!c.package) return + if (utilValues.has(util.name)) return + + const key = `${c.package}@${c.version || ''}` + let mod = npmCache.get(key) + if (!mod) { + const url = c.cdnLink || (c.version ? `https://unpkg.com/${pkg}@${c.version}` : `https://unpkg.com/${pkg}`) + mod = await import(/* @vite-ignore */ url) + npmCache.set(key, mod) + } + let exported: any + if (c.destructuring) { + exported = c.exportName ? mod[c.exportName as keyof typeof mod] : mod + } else { + exported = (c.exportName && mod[c.exportName]) || mod.default || mod + } + if (c.subName && exported) exported = exported[c.subName] + utilValues.set(util.name, exported) +} + +export async function initUtils(utils: Util[] = []) { + if (initialized || loading) { + return + } + loading = true + + for (const util of utils) { + if (util.type === 'npm') { + try { + await loadNpmUtil(util) + } catch (error) { + // eslint-disable-next-line no-console + console.error(`加载 npm 包 ${util.name} 失败:`, error) + } + } else if (util.type === 'function') { + const content = util.content as fnContent + utilValues.set(util.name, parseJSFunction(content)) + } + } + initialized = true + loading = false +} + +export function getUtilsAll() { + return Object.fromEntries(utilValues.entries()) +} diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index 321487115a..dda9e408e2 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -1,10 +1,21 @@ import { ref, computed, readonly } from 'vue' -import type { AppSchema, BlockItem, BlockContent } from '../types/schema' +import type { AppSchema, Util, BlockItem, BlockContent } from '../types/schema' +import { initUtils } from '../app-function/utils' const appSchema = ref(null) const isLoading = ref(false) const error = ref(null) export function useAppSchema() { + // 初始化工具函数 + const initializeUtils = async (utils: Util[]) => { + try { + await initUtils(utils) + } catch (error) { + // eslint-disable-next-line no-console + console.error('工具函数初始化失败:', error) + } + } + // 注入全局CSS const injectGlobalCSS = (css: string) => { if (!css) return @@ -18,6 +29,9 @@ export function useAppSchema() { const initializeAppConfig = async (schema: AppSchema) => { if (!schema?.data) return + // 初始化工具函数 + initializeUtils(schema.data.utils) + // 注入全局CSS injectGlobalCSS(schema.data.css) } diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 9601552d4c..5538fd3179 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -21,6 +21,7 @@ import { useRouter, useRoute } from 'vue-router' import { useAppSchema } from '../composables/useAppSchema' import type { PageContent as Schema } from '../types/schema' import dataSourceMap from '../app-function/dataSource.ts' +import { getUtilsAll } from '../app-function/utils.ts' interface Props { pageId: number @@ -81,7 +82,8 @@ export default defineComponent({ route, router, stores, - dataSourceMap + dataSourceMap, + utils: getUtilsAll() } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 setContext(context, true) From ebbe3753356ae622cb1135d88096522544e3ca18 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 10:39:28 +0800 Subject: [PATCH 13/81] feat: Support internationalization, default to Chinese display --- packages/runtime-renderer/index.ts | 3 ++- .../src/composables/useAppSchema.ts | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index c20b83dbf9..6a00d82136 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -16,6 +16,7 @@ import { createAppRouter } from './src/router' import { createPinia } from 'pinia' import { createStores, generateStoresConfig } from './src/stores' import App from './src/App.vue' +import i18n from '@opentiny/tiny-engine-i18n-host' // 初始化运行时渲染器 export const initRuntimeRenderer = async () => { @@ -31,7 +32,7 @@ export const initRuntimeRenderer = async () => { const stores = createStores(storesConfig, pinia) const app = createApp(App) - app.use(pinia).use(router).mount('#app') + app.use(pinia).use(router).use(i18n).mount('#app') app.provide('stores', stores) return app diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index dda9e408e2..92916638bd 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -1,6 +1,7 @@ import { ref, computed, readonly } from 'vue' -import type { AppSchema, Util, BlockItem, BlockContent } from '../types/schema' +import type { AppSchema, Util, BlockItem, BlockContent, I18nConfig } from '../types/schema' import { initUtils } from '../app-function/utils' +import i18n from '@opentiny/tiny-engine-i18n-host' const appSchema = ref(null) const isLoading = ref(false) @@ -25,10 +26,20 @@ export function useAppSchema() { document.head.appendChild(style) } + const initializeI18n = (i18nConfig: I18nConfig) => { + if (!i18nConfig) return + Object.entries(i18nConfig).forEach(([loc, msgs]) => { + i18n.global.mergeLocaleMessage(loc, msgs as any) + }) + } + // 初始化应用配置 const initializeAppConfig = async (schema: AppSchema) => { if (!schema?.data) return + // 初始化国际化 + initializeI18n(schema.data.i18n) + // 初始化工具函数 initializeUtils(schema.data.utils) From e7212cd20c1c15e6fccce631fdfd5c0552230d1d Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 30 Sep 2025 10:42:44 +0800 Subject: [PATCH 14/81] test: Mock data for test In useAppSchema.ts, replace the actual requests in fetchAppSchema(appId) and fetchBlocks() with mock files to use mock files for testing. --- .../runtime-renderer/src/mock/appSchema.json | 5222 +++++++++++++++++ .../runtime-renderer/src/mock/blocks.json | 380 ++ 2 files changed, 5602 insertions(+) create mode 100644 packages/runtime-renderer/src/mock/appSchema.json create mode 100644 packages/runtime-renderer/src/mock/blocks.json diff --git a/packages/runtime-renderer/src/mock/appSchema.json b/packages/runtime-renderer/src/mock/appSchema.json new file mode 100644 index 0000000000..573989aaca --- /dev/null +++ b/packages/runtime-renderer/src/mock/appSchema.json @@ -0,0 +1,5222 @@ +{ + "data": { + "bridge": [], + "componentsMap": [ + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElInput", + "componentName": "ElInput", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElDatePicker", + "componentName": "ElDatePicker", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElButton", + "componentName": "ElButton", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElForm", + "componentName": "ElForm", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElFormItem", + "componentName": "ElFormItem", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElTable", + "componentName": "ElTable", + "version": "", + "npmrc": null + }, + { + "package": "element-plus", + "destructuring": true, + "exportName": "ElTableColumn", + "componentName": "ElTableColumn", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "CarouselItem", + "componentName": "TinyCarouselItem", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Carousel", + "componentName": "TinyCarousel", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "a", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "[h1, h2, h3, h4, h5, h6]", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "p", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "input", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "video", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "Img", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "button", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "table", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "td", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "form", + "version": "", + "npmrc": null + }, + { + "package": null, + "destructuring": false, + "exportName": null, + "componentName": "label", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "ButtonGroup", + "componentName": "TinyButtonGroup", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Row", + "componentName": "TinyRow", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Layout", + "componentName": "TinyLayout", + "version": "3.20.0", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Form", + "componentName": "TinyForm", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "FormItem", + "componentName": "TinyFormItem", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Col", + "componentName": "TinyCol", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Button", + "componentName": "TinyButton", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Input", + "componentName": "TinyInput", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Radio", + "componentName": "TinyRadio", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Select", + "componentName": "TinySelect", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Switch", + "componentName": "TinySwitch", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Search", + "componentName": "TinySearch", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Checkbox", + "componentName": "TinyCheckbox", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "CheckboxButton", + "componentName": "TinyCheckboxButton", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "CheckboxGroup", + "componentName": "TinyCheckboxGroup", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "DialogBox", + "componentName": "TinyDialogBox", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Tabs", + "componentName": "TinyTabs", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "TabItem", + "componentName": "TinyTabItem", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Breadcrumb", + "componentName": "TinyBreadcrumb", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "BreadcrumbItem", + "componentName": "TinyBreadcrumbItem", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Collapse", + "componentName": "TinyCollapse", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "CollapseItem", + "componentName": "TinyCollapseItem", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Grid", + "componentName": "TinyGrid", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "TinyGridColumn", + "componentName": "TinyGridColumn", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Pager", + "componentName": "TinyPager", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Popeditor", + "componentName": "TinyPopeditor", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Tree", + "componentName": "TinyTree", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "TimeLine", + "componentName": "TinyTimeLine", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Tooltip", + "componentName": "TinyTooltip", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Popover", + "componentName": "TinyPopover", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "DatePicker", + "componentName": "TinyDatePicker", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "Numeric", + "componentName": "TinyNumeric", + "version": "", + "npmrc": null + }, + { + "package": "@opentiny/vue", + "destructuring": true, + "exportName": "TinyTransfer", + "componentName": "TinyTransfer", + "version": "", + "npmrc": null + }, + { + "path": "", + "destructuring": false, + "componentName": "Group1Test1", + "version": "1.0.8", + "dependencies": { + "scripts": [ + { + "package": "@opentiny/vue", + "components": { + "TinyButton": "Button" + } + } + ], + "styles": [] + } + }, + { + "path": "", + "destructuring": false, + "componentName": "Group1Test2", + "version": "1.0.3", + "dependencies": { + "scripts": [ + { + "components": {} + }, + { + "package": "@opentiny/vue", + "components": { + "TinyButton": "Button" + } + } + ], + "styles": [] + } + } + ], + "componentsTree": [ + { + "children": [ + { + "componentName": "div", + "props": { + "style": "padding-bottom: 10px; padding-top: 10px;" + }, + "id": "2b2cabf0", + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "active": "2", + "data": [ + { + "name": "基础配置" + }, + { + "name": "网络配置" + }, + { + "name": "高级配置" + }, + { + "name": "确认配置" + } + ], + "horizontal": true, + "style": "border-radius: 0px;" + }, + "id": "dd764b17" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "id": "30c94cc8", + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "计费模式" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "包年/包月", + "value": "1" + }, + { + "text": "按需计费", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "a8d84361" + } + ], + "id": "9f39f3e7" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "乌兰察布二零一", + "value": "1" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-right: 10px;" + }, + "id": "c97ccd99" + }, + { + "componentName": "Text", + "props": { + "text": "温馨提示:页面左上角切换区域", + "style": "background-color: [object Event]; color: #8a8e99; font-size: 12px;" + }, + "id": "20923497" + }, + { + "componentName": "Text", + "props": { + "text": "不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度", + "style": "display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;" + }, + "id": "54780a26" + } + ], + "id": "4966384d" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "可用区", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "可用区1", + "value": "1" + }, + { + "text": "可用区2", + "value": "2" + }, + { + "text": "可用区3", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "6184481b" + } + ], + "id": "690837bf" + } + ], + "id": "b6a425d4" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "CPU架构" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "x86计算", + "value": "1" + }, + { + "text": "鲲鹏计算", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "7d33ced7" + } + ], + "id": "05ed5a79" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; justify-content: flex-start; align-items: center;" + }, + "id": "606edf78", + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "id": "f3f98246", + "children": [ + { + "componentName": "Text", + "props": { + "text": "vCPUs", + "style": "width: 80px;" + }, + "id": "c287437e" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "4c43286b" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "内存", + "style": "width: 80px; border-radius: 0px;" + }, + "id": "38b8fa1f" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "cd33328e" + } + ], + "id": "2b2c678f" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "规格名称", + "style": "width: 80px;" + }, + "id": "d3eb6352" + }, + { + "componentName": "TinySearch", + "props": { + "modelValue": "", + "placeholder": "输入关键词" + }, + "id": "21cb9282" + } + ], + "id": "b8e0f35c" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-radius: 0px;" + }, + "id": "5000c83e", + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "通用计算型", + "value": "1" + }, + { + "text": "通用计算增强型", + "value": "2" + }, + { + "text": "内存优化型", + "value": "3" + }, + { + "text": "内存优化型", + "value": "4" + }, + { + "text": "磁盘增强型", + "value": "5" + }, + { + "text": "超高I/O型", + "value": "6" + }, + { + "text": "GPU加速型", + "value": "7" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-top: 12px;" + }, + "id": "b8724703" + }, + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "radio", + "width": 60 + }, + { + "field": "employees", + "title": "规格名称" + }, + { + "field": "created_date", + "title": "vCPUs | 内存(GiB)", + "sortable": true + }, + { + "field": "city", + "title": "CPU", + "sortable": true + }, + { + "title": "基准 / 最大带宽\t", + "sortable": true + }, + { + "title": "内网收发包", + "sortable": true + } + ], + "data": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true + } + ], + "style": "margin-top: 12px; border-radius: 0px;", + "auto-resize": true + }, + "id": "77701c25" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; border-radius: 0px;" + }, + "id": "3339838b", + "children": [ + { + "componentName": "Text", + "props": { + "text": "当前规格", + "style": "width: 150px; display: inline-block;" + }, + "id": "203b012b" + }, + { + "componentName": "Text", + "props": { + "text": "通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB", + "style": "font-weight: 700;" + }, + "id": "87723f52" + } + ] + } + ] + } + ], + "id": "657fb2fc" + } + ], + "id": "d19b15cf" + } + ], + "id": "9991228b" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "镜像", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "公共镜像", + "value": "1" + }, + { + "text": "私有镜像", + "value": "2" + }, + { + "text": "共享镜像", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "922b14cb" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "id": "6b679524", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 170px; margin-right: 10px;" + }, + "id": "4851fff7" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 340px;" + }, + "id": "a7183eb7" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px;" + }, + "id": "57aee314", + "children": [ + { + "componentName": "Text", + "props": { + "text": "请注意操作系统的语言类型。", + "style": "color: #e37d29;" + }, + "id": "56d36c27" + } + ] + } + ], + "id": "e3b02436" + } + ], + "id": "59aebf2b" + } + ], + "id": "87ff7b99" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "系统盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex;" + }, + "id": "cddba5b8", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "a97fbe15" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "1cde4c0f" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限240,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px;" + }, + "id": "2815d82d" + } + ] + } + ], + "id": "50239a3a" + } + ], + "id": "e8582986" + }, + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "数据盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; display: flex;" + }, + "id": "728c9825", + "children": [ + { + "componentName": "Icon", + "props": { + "style": "margin-right: 10px; width: 16px; height: 16px;", + "name": "IconPanelMini" + }, + "id": "fded6930" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "62734e3f" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "667c7926" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限600,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px; margin-right: 10px;" + }, + "id": "e7bc36d6" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px;" + }, + "id": "1bd56dc0" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.dataDisk" + } + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPlus", + "style": "width: 16px; height: 16px; margin-right: 10px;" + }, + "id": "65c89f2b" + }, + { + "componentName": "Text", + "props": { + "text": "增加一块数据盘", + "style": "font-size: 12px; border-radius: 0px; margin-right: 10px;" + }, + "id": "cb344071" + }, + { + "componentName": "Text", + "props": { + "text": "您还可以挂载 21 块磁盘(云硬盘)", + "style": "color: #8a8e99; font-size: 12px;" + }, + "id": "80eea996" + } + ], + "id": "e9e530ab" + } + ], + "id": "078e03ef" + } + ], + "id": "ccef886e" + } + ], + "id": "0fb7bd74" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-color: #ffffff; padding-top: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; position: fixed; inset: auto 0% 0% 0%; height: 80px; line-height: 80px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [], + "id": "21ed4475" + }, + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px; height: 100%;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "8" + }, + "id": "b9d051a5", + "children": [ + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "5", + "style": "display: flex;" + }, + "id": "02352776", + "children": [ + { + "componentName": "Text", + "props": { + "text": "购买量", + "style": "margin-right: 10px;" + }, + "id": "0cd9ed5c" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "2f9cf442" + }, + { + "componentName": "Text", + "props": { + "text": "台" + }, + "id": "facd4481" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "7" + }, + "id": "82b6c659", + "children": [ + { + "componentName": "div", + "props": {}, + "id": "9cd65874", + "children": [ + { + "componentName": "Text", + "props": { + "text": "配置费用", + "style": "font-size: 12px;" + }, + "id": "b5a0a0da" + }, + { + "componentName": "Text", + "props": { + "text": "¥1.5776", + "style": "padding-left: 10px; padding-right: 10px; color: #de504e;" + }, + "id": "d9464214" + }, + { + "componentName": "Text", + "props": { + "text": "/小时", + "style": "font-size: 12px;" + }, + "id": "af7cc5e6" + } + ] + }, + { + "componentName": "div", + "props": {}, + "id": "89063830", + "children": [ + { + "componentName": "Text", + "props": { + "text": "参考价格,具体扣费请以账单为准。", + "style": "font-size: 12px; border-radius: 0px;" + }, + "id": "d8995fbc" + }, + { + "componentName": "Text", + "props": { + "text": "了解计费详情", + "style": "font-size: 12px; color: #344899;" + }, + "id": "b383c3e2" + } + ] + } + ] + } + ], + "id": "94fc0e43" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "4", + "style": "display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;" + }, + "id": "10b73009", + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "下一步: 网络配置", + "type": "danger", + "style": "max-width: unset;" + }, + "id": "0b584011" + } + ] + } + ], + "id": "d414a473" + } + ], + "id": "e8ec029b" + } + ], + "css": "body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}", + "componentName": "Page", + "fileName": "createVm", + "lifeCycles": null, + "meta": { + "app": 1, + "gmt_create": "2024-10-16 23:31:48", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2024-10-16 23:31:48", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "createVm", + "depth": 0, + "tenantId": "1", + "name": "createVm", + "siteId": "1", + "page_content": { + "state": { + "dataDisk": [1, 2, 3] + }, + "methods": {}, + "componentName": "Page", + "css": "body {\r\n background-color:#eef0f5 ;\r\n margin-bottom: 80px;\r\n}", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "padding-bottom: 10px; padding-top: 10px;" + }, + "id": "2b2cabf0", + "children": [ + { + "componentName": "TinyTimeLine", + "props": { + "active": "2", + "data": [ + { + "name": "基础配置" + }, + { + "name": "网络配置" + }, + { + "name": "高级配置" + }, + { + "name": "确认配置" + } + ], + "horizontal": true, + "style": "border-radius: 0px;" + }, + "id": "dd764b17" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "id": "30c94cc8", + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "计费模式" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "包年/包月", + "value": "1" + }, + { + "text": "按需计费", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "a8d84361" + } + ], + "id": "9f39f3e7" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "乌兰察布二零一", + "value": "1" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-right: 10px;" + }, + "id": "c97ccd99" + }, + { + "componentName": "Text", + "props": { + "text": "温馨提示:页面左上角切换区域", + "style": "background-color: [object Event]; color: #8a8e99; font-size: 12px;" + }, + "id": "20923497" + }, + { + "componentName": "Text", + "props": { + "text": "不同区域的云服务产品之间内网互不相通;请就近选择靠近您业务的区域,可减少网络时延,提高访问速度", + "style": "display: block; color: #8a8e99; border-radius: 0px; font-size: 12px;" + }, + "id": "54780a26" + } + ], + "id": "4966384d" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "可用区", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "可用区1", + "value": "1" + }, + { + "text": "可用区2", + "value": "2" + }, + { + "text": "可用区3", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "6184481b" + } + ], + "id": "690837bf" + } + ], + "id": "b6a425d4" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "CPU架构" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "x86计算", + "value": "1" + }, + { + "text": "鲲鹏计算", + "value": "2" + } + ], + "modelValue": "1" + }, + "id": "7d33ced7" + } + ], + "id": "05ed5a79" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "区域" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; justify-content: flex-start; align-items: center;" + }, + "id": "606edf78", + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "id": "f3f98246", + "children": [ + { + "componentName": "Text", + "props": { + "text": "vCPUs", + "style": "width: 80px;" + }, + "id": "c287437e" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "4c43286b" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center; margin-right: 10px;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "内存", + "style": "width: 80px; border-radius: 0px;" + }, + "id": "38b8fa1f" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ] + }, + "id": "cd33328e" + } + ], + "id": "2b2c678f" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; align-items: center;" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "规格名称", + "style": "width: 80px;" + }, + "id": "d3eb6352" + }, + { + "componentName": "TinySearch", + "props": { + "modelValue": "", + "placeholder": "输入关键词" + }, + "id": "21cb9282" + } + ], + "id": "b8e0f35c" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "border-radius: 0px;" + }, + "id": "5000c83e", + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "通用计算型", + "value": "1" + }, + { + "text": "通用计算增强型", + "value": "2" + }, + { + "text": "内存优化型", + "value": "3" + }, + { + "text": "内存优化型", + "value": "4" + }, + { + "text": "磁盘增强型", + "value": "5" + }, + { + "text": "超高I/O型", + "value": "6" + }, + { + "text": "GPU加速型", + "value": "7" + } + ], + "modelValue": "1", + "style": "border-radius: 0px; margin-top: 12px;" + }, + "id": "b8724703" + }, + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "radio", + "width": 60 + }, + { + "field": "employees", + "title": "规格名称" + }, + { + "field": "created_date", + "title": "vCPUs | 内存(GiB)", + "sortable": true + }, + { + "field": "city", + "title": "CPU", + "sortable": true + }, + { + "title": "基准 / 最大带宽\t", + "sortable": true + }, + { + "title": "内网收发包", + "sortable": true + } + ], + "data": [ + { + "id": "1", + "name": "GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true + } + ], + "style": "margin-top: 12px; border-radius: 0px;", + "auto-resize": true + }, + "id": "77701c25" + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; border-radius: 0px;" + }, + "id": "3339838b", + "children": [ + { + "componentName": "Text", + "props": { + "text": "当前规格", + "style": "width: 150px; display: inline-block;" + }, + "id": "203b012b" + }, + { + "componentName": "Text", + "props": { + "text": "通用计算型 | Si2.large.2 | 2vCPUs | 4 GiB", + "style": "font-weight: 700;" + }, + "id": "87723f52" + } + ] + } + ] + } + ], + "id": "657fb2fc" + } + ], + "id": "d19b15cf" + } + ], + "id": "9991228b" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "镜像", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyButtonGroup", + "props": { + "data": [ + { + "text": "公共镜像", + "value": "1" + }, + { + "text": "私有镜像", + "value": "2" + }, + { + "text": "共享镜像", + "value": "3" + } + ], + "modelValue": "1" + }, + "id": "922b14cb" + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "id": "6b679524", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 170px; margin-right: 10px;" + }, + "id": "4851fff7" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 340px;" + }, + "id": "a7183eb7" + } + ] + }, + { + "componentName": "div", + "props": { + "style": "margin-top: 12px;" + }, + "id": "57aee314", + "children": [ + { + "componentName": "Text", + "props": { + "text": "请注意操作系统的语言类型。", + "style": "color: #e37d29;" + }, + "id": "56d36c27" + } + ] + } + ], + "id": "e3b02436" + } + ], + "id": "59aebf2b" + } + ], + "id": "87ff7b99" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-radius: 4px; border-color: #fff; padding-top: 10px; padding-bottom: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; margin-bottom: 10px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "系统盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "display: flex;" + }, + "id": "cddba5b8", + "children": [ + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "a97fbe15" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "1cde4c0f" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限240,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px;" + }, + "id": "2815d82d" + } + ] + } + ], + "id": "50239a3a" + } + ], + "id": "e8582986" + }, + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "数据盘", + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "div", + "props": { + "style": "margin-top: 12px; display: flex;" + }, + "id": "728c9825", + "children": [ + { + "componentName": "Icon", + "props": { + "style": "margin-right: 10px; width: 16px; height: 16px;", + "name": "IconPanelMini" + }, + "id": "fded6930" + }, + { + "componentName": "TinySelect", + "props": { + "modelValue": "", + "placeholder": "请选择", + "options": [ + { + "value": "1", + "label": "黄金糕" + }, + { + "value": "2", + "label": "双皮奶" + } + ], + "style": "width: 200px; margin-right: 10px;" + }, + "id": "62734e3f" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "667c7926" + }, + { + "componentName": "Text", + "props": { + "text": "GiB \nIOPS上限600,IOPS突发上限5,000", + "style": "color: #575d6c; font-size: 12px; margin-right: 10px;" + }, + "id": "e7bc36d6" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px;" + }, + "id": "1bd56dc0" + } + ], + "loop": { + "type": "JSExpression", + "value": "this.state.dataDisk" + } + }, + { + "componentName": "div", + "props": { + "style": "display: flex; margin-top: 12px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPlus", + "style": "width: 16px; height: 16px; margin-right: 10px;" + }, + "id": "65c89f2b" + }, + { + "componentName": "Text", + "props": { + "text": "增加一块数据盘", + "style": "font-size: 12px; border-radius: 0px; margin-right: 10px;" + }, + "id": "cb344071" + }, + { + "componentName": "Text", + "props": { + "text": "您还可以挂载 21 块磁盘(云硬盘)", + "style": "color: #8a8e99; font-size: 12px;" + }, + "id": "80eea996" + } + ], + "id": "e9e530ab" + } + ], + "id": "078e03ef" + } + ], + "id": "ccef886e" + } + ], + "id": "0fb7bd74" + }, + { + "componentName": "div", + "props": { + "style": "border-width: 1px; border-style: solid; border-color: #ffffff; padding-top: 10px; padding-left: 10px; padding-right: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; background-color: #fff; position: fixed; inset: auto 0% 0% 0%; height: 80px; line-height: 80px; border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "inline": false, + "label-position": "left ", + "label-width": "150px", + "style": "border-radius: 0px;" + }, + "children": [], + "id": "21ed4475" + }, + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px; height: 100%;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "8" + }, + "id": "b9d051a5", + "children": [ + { + "componentName": "TinyRow", + "props": { + "style": "border-radius: 0px;" + }, + "children": [ + { + "componentName": "TinyCol", + "props": { + "span": "5", + "style": "display: flex;" + }, + "id": "02352776", + "children": [ + { + "componentName": "Text", + "props": { + "text": "购买量", + "style": "margin-right: 10px;" + }, + "id": "0cd9ed5c" + }, + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "style": "width: 120px; margin-right: 10px;" + }, + "id": "2f9cf442" + }, + { + "componentName": "Text", + "props": { + "text": "台" + }, + "id": "facd4481" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "7" + }, + "id": "82b6c659", + "children": [ + { + "componentName": "div", + "props": {}, + "id": "9cd65874", + "children": [ + { + "componentName": "Text", + "props": { + "text": "配置费用", + "style": "font-size: 12px;" + }, + "id": "b5a0a0da" + }, + { + "componentName": "Text", + "props": { + "text": "¥1.5776", + "style": "padding-left: 10px; padding-right: 10px; color: #de504e;" + }, + "id": "d9464214" + }, + { + "componentName": "Text", + "props": { + "text": "/小时", + "style": "font-size: 12px;" + }, + "id": "af7cc5e6" + } + ] + }, + { + "componentName": "div", + "props": {}, + "id": "89063830", + "children": [ + { + "componentName": "Text", + "props": { + "text": "参考价格,具体扣费请以账单为准。", + "style": "font-size: 12px; border-radius: 0px;" + }, + "id": "d8995fbc" + }, + { + "componentName": "Text", + "props": { + "text": "了解计费详情", + "style": "font-size: 12px; color: #344899;" + }, + "id": "b383c3e2" + } + ] + } + ] + } + ], + "id": "94fc0e43" + } + ] + }, + { + "componentName": "TinyCol", + "props": { + "span": "4", + "style": "display: flex; flex-direction: row-reverse; border-radius: 0px; height: 100%; justify-content: flex-start; align-items: center;" + }, + "id": "10b73009", + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "下一步: 网络配置", + "type": "danger", + "style": "max-width: unset;" + }, + "id": "0b584011" + } + ] + } + ], + "id": "d414a473" + } + ], + "id": "e8ec029b" + } + ], + "fileName": "createVm" + }, + "id": 1, + "isPage": true, + "group": "staticPages" + }, + "methods": {}, + "props": {}, + "state": { + "dataDisk": [1, 2, 3] + } + }, + { + "children": [ + { + "componentName": "div", + "props": { + "style": "text-align: center; padding: 8px 12px; box-shadow: 0 0 4px #0003;", + "className": "component-base-style" + }, + "children": [ + { + "componentName": "RouterLink", + "props": { + "to": { + "name": "9" + }, + "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPublicHome", + "style": "margin-top: 3px;" + }, + "id": "2143f42d" + }, + { + "componentName": "Text", + "props": { + "text": "/fivea" + }, + "id": "11324214" + } + ], + "id": "66622445" + }, + { + "componentName": "RouterLink", + "props": { + "to": { + "name": "10" + }, + "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconTaskCooperation", + "style": "margin-top: 3px;" + }, + "id": "25553142" + }, + { + "componentName": "Text", + "props": { + "text": "/fiveb" + }, + "id": "22525242" + } + ], + "id": "56426524" + } + ], + "id": "4267e3f2" + }, + { + "componentName": "RouterView", + "props": { + "className": "component-base-style" + }, + "children": [], + "id": "42524344" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "Tfive", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-09-16 14:38:32", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-09-16 14:38:32", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "Tfive", + "depth": 0, + "tenantId": "1", + "name": "Tfive", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "div", + "props": { + "style": "text-align: center; padding: 8px 12px; box-shadow: 0 0 4px #0003;", + "className": "component-base-style" + }, + "children": [ + { + "componentName": "RouterLink", + "props": { + "to": { + "name": "9" + }, + "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconPublicHome", + "style": "margin-top: 3px;" + }, + "id": "2143f42d" + }, + { + "componentName": "Text", + "props": { + "text": "/fivea" + }, + "id": "11324214" + } + ], + "id": "66622445" + }, + { + "componentName": "RouterLink", + "props": { + "to": { + "name": "10" + }, + "style": "display: inline-flex; gap: 8px; padding: 10px 20px; color: inherit; text-decoration: none;" + }, + "children": [ + { + "componentName": "Icon", + "props": { + "name": "IconTaskCooperation", + "style": "margin-top: 3px;" + }, + "id": "25553142" + }, + { + "componentName": "Text", + "props": { + "text": "/fiveb" + }, + "id": "22525242" + } + ], + "id": "56426524" + } + ], + "id": "4267e3f2" + }, + { + "componentName": "RouterView", + "props": { + "className": "component-base-style" + }, + "children": [], + "id": "42524344" + } + ], + "dataSource": { + "list": [] + }, + "state": { + "textTvie": { + "text": "Tfive" + } + }, + "methods": {}, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "Tfive", + "id": "body" + }, + "id": 8, + "isPage": true, + "group": "staticPages" + }, + "methods": {}, + "props": { + "className": "page-base-style" + }, + "state": { + "textTvie": { + "text": "Tfive" + } + } + }, + { + "children": [ + { + "componentName": "Group1Test2", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "c4866555" + }, + { + "componentName": "Group1Test1", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "24622349" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "Tfour", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-08-29 17:59:23", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-08-29 17:59:23", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "tfour", + "depth": 0, + "tenantId": "1", + "name": "Tfour", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "Group1Test2", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "c4866555" + }, + { + "componentName": "Group1Test1", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "24622349" + } + ], + "dataSource": { + "list": [] + }, + "state": {}, + "methods": {}, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "Tfour", + "id": "body" + }, + "id": 6, + "isPage": true, + "group": "staticPages" + }, + "methods": {}, + "props": { + "className": "page-base-style" + }, + "state": {} + }, + { + "children": [ + { + "componentName": "ElDatePicker", + "props": { + "className": "component-base-style" + }, + "children": [], + "id": "e5239a66" + }, + { + "componentName": "ElForm", + "children": [ + { + "componentName": "ElFormItem", + "props": { + "label": "账号", + "prop": "account" + }, + "children": [ + { + "componentName": "ElInput", + "props": { + "modelValue": "", + "placeholder": "请输入账号" + }, + "id": "33253586" + } + ], + "id": "5c745f66" + }, + { + "componentName": "ElFormItem", + "props": { + "label": "密码", + "prop": "password" + }, + "children": [ + { + "componentName": "ElInput", + "props": { + "modelValue": "", + "placeholder": "请输入密码", + "type": "password" + }, + "id": "e5654633" + } + ], + "id": "42145465" + }, + { + "componentName": "ElFormItem", + "props": {}, + "children": [ + { + "componentName": "ElButton", + "props": { + "type": "primary", + "style": "margin-right: 10px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "提交" + }, + "id": "35376252" + } + ], + "id": "f51fbc69" + }, + { + "componentName": "ElButton", + "props": { + "type": "primary" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "重置" + }, + "id": "49346333" + } + ], + "id": "24245522" + } + ], + "id": "56fa5244" + } + ], + "props": { + "className": "component-base-style" + }, + "id": "1d554376" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "3" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "ttwo" + }, + "id": "392555b2" + } + ], + "id": "5d9a6439" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "Tone", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-08-27 00:50:18", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-08-27 00:50:18", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "tone", + "depth": 0, + "tenantId": "1", + "name": "Tone", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "ElDatePicker", + "props": { + "className": "component-base-style" + }, + "children": [], + "id": "e5239a66" + }, + { + "componentName": "ElForm", + "children": [ + { + "componentName": "ElFormItem", + "props": { + "label": "账号", + "prop": "account" + }, + "children": [ + { + "componentName": "ElInput", + "props": { + "modelValue": "", + "placeholder": "请输入账号" + }, + "id": "33253586" + } + ], + "id": "5c745f66" + }, + { + "componentName": "ElFormItem", + "props": { + "label": "密码", + "prop": "password" + }, + "children": [ + { + "componentName": "ElInput", + "props": { + "modelValue": "", + "placeholder": "请输入密码", + "type": "password" + }, + "id": "e5654633" + } + ], + "id": "42145465" + }, + { + "componentName": "ElFormItem", + "props": {}, + "children": [ + { + "componentName": "ElButton", + "props": { + "type": "primary", + "style": "margin-right: 10px" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "提交" + }, + "id": "35376252" + } + ], + "id": "f51fbc69" + }, + { + "componentName": "ElButton", + "props": { + "type": "primary" + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "重置" + }, + "id": "49346333" + } + ], + "id": "24245522" + } + ], + "id": "56fa5244" + } + ], + "props": { + "className": "component-base-style" + }, + "id": "1d554376" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "3" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "ttwo" + }, + "id": "392555b2" + } + ], + "id": "5d9a6439" + } + ], + "dataSource": { + "list": [] + }, + "state": {}, + "methods": { + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n console.log('123456')\n this.state.state1.button++\n console.log(this.state.state1.button)\n}\n" + } + }, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "Tone", + "id": "body" + }, + "id": 2, + "isPage": true, + "group": "staticPages" + }, + "methods": { + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n console.log('123456')\n this.state.state1.button++\n console.log(this.state.state1.button)\n}\n" + } + }, + "props": { + "className": "page-base-style" + }, + "state": {} + }, + { + "children": [ + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 1 + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "title": "姓名", + "field": "2" + }, + { + "title": "序号", + "field": "1" + } + ], + "className": "component-base-style", + "fetchData": { + "type": "JSExpression", + "value": "{ api: this.getTableData536be324 }" + }, + "pager": { + "attrs": { + "currentPage": 1, + "pageSize": 50, + "pageSizes": [10, 20, 50], + "total": 0, + "layout": "sizes,total, prev, pager, next, jumper" + } + } + }, + "children": [], + "id": "33675561" + } + ], + "id": "536be324" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "2" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/tone:测试element-plus组件渲染 " + }, + "id": "c5361165", + "children": [] + } + ], + "id": "52344241" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "6" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/tfour: 测试两个区块同时渲染" + }, + "id": "7336d695" + } + ], + "id": "222c2483" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "1" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/createVm" + }, + "id": "51666233" + } + ], + "id": "33632634" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "10" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "tfive/fiveb嵌套路由" + }, + "id": "45545223" + } + ], + "id": "45462c3c" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": "路由至tzero进行多项测试", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "45324446" + } + ], + "id": "36665265" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "Ttwo", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-08-27 10:40:28", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": true, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-08-27 10:40:28", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "ttwo", + "depth": 0, + "tenantId": "1", + "name": "Ttwo", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 1 + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "title": "姓名", + "field": "2" + }, + { + "title": "序号", + "field": "1" + } + ], + "className": "component-base-style", + "fetchData": { + "type": "JSExpression", + "value": "{ api: this.getTableData536be324 }" + }, + "pager": { + "attrs": { + "currentPage": 1, + "pageSize": 50, + "pageSizes": [10, 20, 50], + "total": 0, + "layout": "sizes,total, prev, pager, next, jumper" + } + } + }, + "children": [], + "id": "33675561" + } + ], + "id": "536be324" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "2" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/tone:测试element-plus组件渲染 " + }, + "id": "c5361165", + "children": [] + } + ], + "id": "52344241" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "6" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/tfour: 测试两个区块同时渲染" + }, + "id": "7336d695" + } + ], + "id": "222c2483" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "1" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "/createVm" + }, + "id": "51666233" + } + ], + "id": "33632634" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "10" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "tfive/fiveb嵌套路由" + }, + "id": "45545223" + } + ], + "id": "45462c3c" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": "路由至tzero进行多项测试", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "45324446" + } + ], + "id": "36665265" + } + ], + "dataSource": { + "list": [] + }, + "state": { + "state1": { + "button": 1 + } + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.router.push('/tzero')\n}\n" + }, + "getTableData536be324": { + "type": "JSFunction", + "value": "function getTableData536be324({ page, sort, sortBy, filters }) {\n /**\n * @param {Object} sort 排序数据\n * @param {Array} sortBy 排序方式\n * @param {Object} page 分页数据\n * @param {Array} filters 筛选数据\n * @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } }\n */\n return new Promise((resolve, reject) => {\n this.dataSourceMap.tableTest1.load().then((res) => {\n // 如果按照数据源面板的建议格式编写dataHandler\n // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} }\n resolve({ result: res.data.items, page: { total: res.data.total } })\n })\n })\n}\n" + }, + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n this.state.button++\n}\n" + } + }, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "Ttwo", + "id": "body" + }, + "id": 3, + "isPage": true, + "group": "staticPages" + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.router.push('/tzero')\n}\n" + }, + "getTableData536be324": { + "type": "JSFunction", + "value": "function getTableData536be324({ page, sort, sortBy, filters }) {\n /**\n * @param {Object} sort 排序数据\n * @param {Array} sortBy 排序方式\n * @param {Object} page 分页数据\n * @param {Array} filters 筛选数据\n * @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } }\n */\n return new Promise((resolve, reject) => {\n this.dataSourceMap.tableTest1.load().then((res) => {\n // 如果按照数据源面板的建议格式编写dataHandler\n // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} }\n resolve({ result: res.data.items, page: { total: res.data.total } })\n })\n })\n}\n" + }, + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n this.state.button++\n}\n" + } + }, + "props": { + "className": "page-base-style" + }, + "state": { + "state1": { + "button": 1 + } + } + }, + { + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "h1", + "props": { + "className": "component-base-style" + }, + "children": "test-page-0", + "id": "32628545" + } + ], + "id": "64416324" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[utils测试]", + "className": "component-base-style" + }, + "children": [], + "id": "d32123e2" + }, + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.testUtils" + }, + "text": "测试function和npm包的导入" + }, + "children": [], + "id": "15662591" + } + ], + "id": "644ba646" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[state测试]:点击增加button计数", + "className": "component-base-style" + }, + "children": [], + "id": "1e5366a5" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.deep.deepButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "45a53354" + }, + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": "点击抛出错误", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew1" + } + }, + "children": [], + "id": "36141245" + } + ], + "id": "4355544e" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "页面state1的getter测试", + "className": "component-base-style" + }, + "children": [], + "id": "3463e661" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.name" + }, + "className": "component-base-style" + }, + "children": [], + "id": "45212416" + } + ], + "id": "65313356" + } + ], + "id": "553d1e44" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[stores测试]:在函数中调用actions", + "className": "component-base-style" + }, + "children": [], + "id": "326f2132" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "stores的state点击计数器button" + }, + "children": [], + "id": "55593d53" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.stores.ttest1.button" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNewStoresButton" + } + }, + "children": [], + "id": "5524246f" + } + ], + "id": "651c5121" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "在getters中定义的doubleButton" + }, + "children": [], + "id": "f342463a" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.stores.ttest1.doubleButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickGetters" + } + }, + "children": [], + "id": "59445645" + } + ], + "id": "226463d3" + } + ], + "id": "a2424415" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[路由测试]", + "className": "component-base-style" + }, + "children": [], + "id": "3c11e664" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "8" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "路由到嵌套路由Tfive,默认跳转页为Tfive/fiveA" + }, + "id": "5251f652" + } + ], + "id": "5fd536d8" + } + ], + "id": "21af6265" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "[国际化测试]:" + }, + "children": [], + "id": "25661983" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": { + "type": "JSExpression", + "value": "t('lowcode.26831252')" + } + }, + "children": [], + "id": "32254462" + } + ], + "id": "25144234" + }, + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 1 + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "className": "component-base-style", + "fetchData": { + "type": "JSExpression", + "value": "{ api: this.getTableData41564366 }" + }, + "columns": [ + { + "title": "姓名", + "field": "2" + }, + { + "title": "序号", + "field": "1" + } + ], + "pager": { + "attrs": { + "currentPage": 1, + "pageSize": 50, + "pageSizes": [10, 20, 50], + "total": 0, + "layout": "sizes,total, prev, pager, next, jumper" + } + } + }, + "children": [], + "id": "54659464" + } + ], + "id": "41564366" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "[远程数据源测试]" + }, + "children": [], + "id": "562b6e54" + }, + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.datasourceRemoteTest.success", + "model": true + }, + "className": "component-base-style" + }, + "children": [], + "id": "7f57662f" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "是否存在页面id为7的页面" + }, + "children": [], + "id": "72296416" + } + ], + "id": "24125532" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.deep.deepButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew2" + } + }, + "children": [], + "id": "612645a7" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "打印数据源.load()方法返回结果" + }, + "children": [], + "id": "2826635a" + } + ], + "id": "1e5292df" + }, + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 2 + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": { + "type": "JSExpression", + "value": "this.state.datasourceRemoteTest.code" + }, + "className": "component-base-style" + }, + "children": [], + "id": "22325335", + "loop": { + "type": "JSExpression", + "value": "this.state.loop7286563f" + } + } + ], + "id": "7286563f" + }, + { + "componentName": "Group1Test2", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "46333445" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "Tzero", + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function setup({\n props,\n state,\n watch,\n onMounted\n}) {\n console.log('[setup]');\n\n /** start-datasourceTableTest1 设计器生成的代码,为了避免出现问题,请勿修改 */\n this.dataSourceMap.tableTest1.load().then(res => {\n state.datasourceTableTest1 = res?.data?.items || res?.data || res;\n });\n /** end-datasourceTableTest1 */\n\n /** start-datasourceRemoteTest 设计器生成的代码,为了避免出现问题,请勿修改 */\n this.dataSourceMap.remoteTest.load().then(res => {\n state.datasourceRemoteTest = res?.data?.items || res?.data || res;\n });\n /** end-datasourceRemoteTest */\n\n /** start-datasourceLocalhost 设计器生成的代码,为了避免出现问题,请勿修改 */\n state.intervalIdLocalhost = setInterval(() => {\n this.dataSourceMap.localhost.load().then(res => {\n state.datasourceLocalhost = res?.data?.items || res?.data || res\n })\n }, 5000);\n /** end-datasourceLocalhost */\n \n return 0\n}" + }, + "onBeforeMount": { + "type": "JSFunction", + "value": "function onBeforeMount() {\r\n console.log('[onBeforeMount]')\r\n return 0\r\n} " + }, + "onMounted": { + "type": "JSFunction", + "value": "function onMounted() {\r\n console.log('[onMounted]')\r\n return 0\r\n} " + }, + "onUpdated": { + "type": "JSFunction", + "value": "function onUpdated() {\r\n console.log('[onUpdated]')\r\n} " + }, + "onBeforeUpdate": { + "type": "JSFunction", + "value": "function onBeforeUpdate() { \r\n console.log('onBeforeUpdated')\r\n} " + }, + "onBeforeUnmount": { + "type": "JSFunction", + "value": "function onBeforeUnmount() { \r\n console.log('onBeforeUnmounted')\r\n} " + }, + "onUnmounted": { + "type": "JSFunction", + "value": "function onUnmounted() {\n console.log('onUnmounted')\n\n /** start-datasourceRemoteTest 设计器生成的代码,为了避免出现问题,请勿修改 */\n clearInterval(this.state.intervalIdRemoteTest);\n /** end-datasourceRemoteTest */\n\n /** start-datasourceLocalhost 设计器生成的代码,为了避免出现问题,请勿修改 */\n clearInterval(state.intervalIdLocalhost);\n /** end-datasourceLocalhost */}" + }, + "onErrorCaptured": { + "type": "JSFunction", + "value": "function onErrorCaptured(err) {\r\n console.error(err)\r\n console.log('[onErrorCaptured]')\r\n return false\r\n} " + }, + "onActivated": { + "type": "JSFunction", + "value": "function onActivated() { \r\n console.log('[onActivated]')\r\n} " + }, + "onDeactivated": { + "type": "JSFunction", + "value": "function onDeactivated() { \r\n console.log('[onDeactivated]')\r\n} " + } + }, + "meta": { + "app": 1, + "gmt_create": "2025-08-28 19:27:25", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-08-28 19:27:25", + "parentId": "0", + "occupierBy": "1", + "isDefault": false, + "router": "tzero", + "depth": 0, + "tenantId": "1", + "name": "Tzero", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style", + "firstname": "", + "lastname": "" + }, + "lifeCycles": { + "setup": { + "type": "JSFunction", + "value": "function setup({\n props,\n state,\n watch,\n onMounted\n}) {\n console.log('[setup]');\n\n /** start-datasourceTableTest1 设计器生成的代码,为了避免出现问题,请勿修改 */\n this.dataSourceMap.tableTest1.load().then(res => {\n state.datasourceTableTest1 = res?.data?.items || res?.data || res;\n });\n /** end-datasourceTableTest1 */\n\n /** start-datasourceRemoteTest 设计器生成的代码,为了避免出现问题,请勿修改 */\n this.dataSourceMap.remoteTest.load().then(res => {\n state.datasourceRemoteTest = res?.data?.items || res?.data || res;\n });\n /** end-datasourceRemoteTest */\n\n /** start-datasourceLocalhost 设计器生成的代码,为了避免出现问题,请勿修改 */\n state.intervalIdLocalhost = setInterval(() => {\n this.dataSourceMap.localhost.load().then(res => {\n state.datasourceLocalhost = res?.data?.items || res?.data || res\n })\n }, 5000);\n /** end-datasourceLocalhost */\n \n return 0\n}" + }, + "onBeforeMount": { + "type": "JSFunction", + "value": "function onBeforeMount() {\r\n console.log('[onBeforeMount]')\r\n return 0\r\n} " + }, + "onMounted": { + "type": "JSFunction", + "value": "function onMounted() {\r\n console.log('[onMounted]')\r\n return 0\r\n} " + }, + "onUpdated": { + "type": "JSFunction", + "value": "function onUpdated() {\r\n console.log('[onUpdated]')\r\n} " + }, + "onBeforeUpdate": { + "type": "JSFunction", + "value": "function onBeforeUpdate() { \r\n console.log('onBeforeUpdated')\r\n} " + }, + "onBeforeUnmount": { + "type": "JSFunction", + "value": "function onBeforeUnmount() { \r\n console.log('onBeforeUnmounted')\r\n} " + }, + "onUnmounted": { + "type": "JSFunction", + "value": "function onUnmounted() {\n console.log('onUnmounted')\n\n /** start-datasourceRemoteTest 设计器生成的代码,为了避免出现问题,请勿修改 */\n clearInterval(this.state.intervalIdRemoteTest);\n /** end-datasourceRemoteTest */\n\n /** start-datasourceLocalhost 设计器生成的代码,为了避免出现问题,请勿修改 */\n clearInterval(state.intervalIdLocalhost);\n /** end-datasourceLocalhost */}" + }, + "onErrorCaptured": { + "type": "JSFunction", + "value": "function onErrorCaptured(err) {\r\n console.error(err)\r\n console.log('[onErrorCaptured]')\r\n return false\r\n} " + }, + "onActivated": { + "type": "JSFunction", + "value": "function onActivated() { \r\n console.log('[onActivated]')\r\n} " + }, + "onDeactivated": { + "type": "JSFunction", + "value": "function onDeactivated() { \r\n console.log('[onDeactivated]')\r\n} " + } + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "h1", + "props": { + "className": "component-base-style" + }, + "children": "test-page-0", + "id": "32628545" + } + ], + "id": "64416324" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[utils测试]", + "className": "component-base-style" + }, + "children": [], + "id": "d32123e2" + }, + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.testUtils" + }, + "text": "测试function和npm包的导入" + }, + "children": [], + "id": "15662591" + } + ], + "id": "644ba646" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[state测试]:点击增加button计数", + "className": "component-base-style" + }, + "children": [], + "id": "1e5366a5" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.deep.deepButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "45a53354" + }, + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": "点击抛出错误", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew1" + } + }, + "children": [], + "id": "36141245" + } + ], + "id": "4355544e" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "页面state1的getter测试", + "className": "component-base-style" + }, + "children": [], + "id": "3463e661" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.name" + }, + "className": "component-base-style" + }, + "children": [], + "id": "45212416" + } + ], + "id": "65313356" + } + ], + "id": "553d1e44" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[stores测试]:在函数中调用actions", + "className": "component-base-style" + }, + "children": [], + "id": "326f2132" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "stores的state点击计数器button" + }, + "children": [], + "id": "55593d53" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.stores.ttest1.button" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNewStoresButton" + } + }, + "children": [], + "id": "5524246f" + } + ], + "id": "651c5121" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "在getters中定义的doubleButton" + }, + "children": [], + "id": "f342463a" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.stores.ttest1.doubleButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickGetters" + } + }, + "children": [], + "id": "59445645" + } + ], + "id": "226463d3" + } + ], + "id": "a2424415" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "[路由测试]", + "className": "component-base-style" + }, + "children": [], + "id": "3c11e664" + }, + { + "componentName": "RouterLink", + "props": { + "className": "component-base-style", + "to": { + "name": "8" + } + }, + "children": [ + { + "componentName": "Text", + "props": { + "text": "路由到嵌套路由Tfive,默认跳转页为Tfive/fiveA" + }, + "id": "5251f652" + } + ], + "id": "5fd536d8" + } + ], + "id": "21af6265" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "[国际化测试]:" + }, + "children": [], + "id": "25661983" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": { + "type": "JSExpression", + "value": "t('lowcode.26831252')" + } + }, + "children": [], + "id": "32254462" + } + ], + "id": "25144234" + }, + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 1 + }, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "className": "component-base-style", + "fetchData": { + "type": "JSExpression", + "value": "{ api: this.getTableData41564366 }" + }, + "columns": [ + { + "title": "姓名", + "field": "2" + }, + { + "title": "序号", + "field": "1" + } + ], + "pager": { + "attrs": { + "currentPage": 1, + "pageSize": 50, + "pageSizes": [10, 20, 50], + "total": 0, + "layout": "sizes,total, prev, pager, next, jumper" + } + } + }, + "children": [], + "id": "54659464" + } + ], + "id": "41564366" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "[远程数据源测试]" + }, + "children": [], + "id": "562b6e54" + }, + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "this.state.datasourceRemoteTest.success", + "model": true + }, + "className": "component-base-style" + }, + "children": [], + "id": "7f57662f" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "是否存在页面id为7的页面" + }, + "children": [], + "id": "72296416" + } + ], + "id": "24125532" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.state1.deep.deepButton" + }, + "className": "component-base-style", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew2" + } + }, + "children": [], + "id": "612645a7" + }, + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "打印数据源.load()方法返回结果" + }, + "children": [], + "id": "2826635a" + } + ], + "id": "1e5292df" + }, + { + "componentName": "Collection", + "props": { + "className": "component-base-style", + "dataSource": 2 + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": { + "type": "JSExpression", + "value": "this.state.datasourceRemoteTest.code" + }, + "className": "component-base-style" + }, + "children": [], + "id": "22325335", + "loop": { + "type": "JSExpression", + "value": "this.state.loop7286563f" + } + } + ], + "id": "7286563f" + }, + { + "componentName": "Group1Test2", + "props": { + "className": "block-base-style" + }, + "componentType": "Block", + "children": [], + "id": "46333445" + } + ], + "dataSource": { + "list": [] + }, + "state": { + "state1": { + "defaultValue": { + "name": "", + "len1": "", + "len2": "", + "button": 1, + "switch": true, + "deep": { + "deepButton": 1 + } + }, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.state.state1.name=`${this.state.state2.firstname} ${this.state.state2.lastname}`\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n}" + } + } + }, + "state2": { + "firstname": "F-name", + "lastname": "L-name" + }, + "datasourceTableTest1": [ + { + "1": 1, + "2": "张三" + } + ], + "datasourceRemoteTest": [], + "loopb6f43413": [], + "loop7286563f": [], + "datasourceLocalhost": [] + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.state.state1.deep.deepButton++\n console.log(`[count_button]${this.state.state1.deep.deepButton}`)\n}\n" + }, + "onClickNewStoresButton": { + "type": "JSFunction", + "value": "function onClickNewStoresButton(event) {\n this.stores.ttest1.increcement()\n}\n" + }, + "getTableData41564366": { + "type": "JSFunction", + "value": "function getTableData41564366({ page, sort, sortBy, filters }) {\n /**\n * @param {Object} sort 排序数据\n * @param {Array} sortBy 排序方式\n * @param {Object} page 分页数据\n * @param {Array} filters 筛选数据\n * @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } }\n */\n return new Promise((resolve, reject) => {\n this.dataSourceMap.tableTest1.load().then((res) => {\n // 如果按照数据源面板的建议格式编写dataHandler\n // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} }\n resolve({ result: res.data.items, page: { total: res.data.total } })\n })\n })\n}\n" + }, + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n throw Error('模拟错误测试')\n}\n" + }, + "onClickNew2": { + "type": "JSFunction", + "value": "function onClickNew2(event) {\n this.state.state1.deep.deepButton++\n console.log('onClickNew2执行')\n console.log('this.dataSourceMap.tableTest1.load()', this.dataSourceMap.tableTest1.load())\n console.log('this.dataSourceMap.remoteTest.load()', this.dataSourceMap.remoteTest.load())\n console.log('this.state.datasourceRemoteTest.code', this.state.datasourceRemoteTest.code)\n}\n" + }, + "onClickGetters": { + "type": "JSFunction", + "value": "function onClickGetters(event) {\n this.stores.ttest1.consoleTest()\n console.log(`this.stores.ttest1.button: ${this.stores.ttest1.button}`)\n console.log(`this.stores.ttest1.doubleButton: ${this.stores.ttest1.doubleButton}`)\n}\n" + }, + "testUtils": { + "type": "JSFunction", + "value": "function testUtils(event) {\n const numbers = [2, 1, 2, 3, 3, 4, 5, 4]\n console.log('original numbers: ', numbers)\n const uniqueNumbers = this.utils.uniq(numbers)\n console.log('numbers handled by uniq() which is exported from lodash: ', uniqueNumbers)\n this.utils.testFunction()\n console.log('this.utils.axios', this.utils.axios)\n}\n" + } + }, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "Tzero", + "id": "body" + }, + "id": 5, + "isPage": true, + "group": "staticPages" + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n this.state.state1.deep.deepButton++\n console.log(`[count_button]${this.state.state1.deep.deepButton}`)\n}\n" + }, + "onClickNewStoresButton": { + "type": "JSFunction", + "value": "function onClickNewStoresButton(event) {\n this.stores.ttest1.increcement()\n}\n" + }, + "getTableData41564366": { + "type": "JSFunction", + "value": "function getTableData41564366({ page, sort, sortBy, filters }) {\n /**\n * @param {Object} sort 排序数据\n * @param {Array} sortBy 排序方式\n * @param {Object} page 分页数据\n * @param {Array} filters 筛选数据\n * @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } }\n */\n return new Promise((resolve, reject) => {\n this.dataSourceMap.tableTest1.load().then((res) => {\n // 如果按照数据源面板的建议格式编写dataHandler\n // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} }\n resolve({ result: res.data.items, page: { total: res.data.total } })\n })\n })\n}\n" + }, + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n throw Error('模拟错误测试')\n}\n" + }, + "onClickNew2": { + "type": "JSFunction", + "value": "function onClickNew2(event) {\n this.state.state1.deep.deepButton++\n console.log('onClickNew2执行')\n console.log('this.dataSourceMap.tableTest1.load()', this.dataSourceMap.tableTest1.load())\n console.log('this.dataSourceMap.remoteTest.load()', this.dataSourceMap.remoteTest.load())\n console.log('this.state.datasourceRemoteTest.code', this.state.datasourceRemoteTest.code)\n}\n" + }, + "onClickGetters": { + "type": "JSFunction", + "value": "function onClickGetters(event) {\n this.stores.ttest1.consoleTest()\n console.log(`this.stores.ttest1.button: ${this.stores.ttest1.button}`)\n console.log(`this.stores.ttest1.doubleButton: ${this.stores.ttest1.doubleButton}`)\n}\n" + }, + "testUtils": { + "type": "JSFunction", + "value": "function testUtils(event) {\n const numbers = [2, 1, 2, 3, 3, 4, 5, 4]\n console.log('original numbers: ', numbers)\n const uniqueNumbers = this.utils.uniq(numbers)\n console.log('numbers handled by uniq() which is exported from lodash: ', uniqueNumbers)\n this.utils.testFunction()\n console.log('this.utils.axios', this.utils.axios)\n}\n" + } + }, + "props": { + "className": "page-base-style", + "firstname": "", + "lastname": "" + }, + "state": { + "state1": { + "defaultValue": { + "name": "", + "len1": "", + "len2": "", + "button": 1, + "switch": true, + "deep": { + "deepButton": 1 + } + }, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function getter() {\r\n this.state.state1.name=`${this.state.state2.firstname} ${this.state.state2.lastname}`\r\n}" + }, + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n}" + } + } + }, + "state2": { + "firstname": "F-name", + "lastname": "L-name" + }, + "datasourceTableTest1": [ + { + "1": 1, + "2": "张三" + } + ], + "datasourceRemoteTest": [], + "loopb6f43413": [], + "loop7286563f": [], + "datasourceLocalhost": [] + } + }, + { + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "This is page A(默认路由页面)", + "className": "component-base-style" + }, + "children": [], + "id": "622454f3" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "FiveA", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-09-16 14:39:19", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-09-16 14:39:19", + "parentId": "8", + "occupierBy": "1", + "isDefault": true, + "router": "fivea", + "depth": 0, + "tenantId": "1", + "name": "FiveA", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "This is page A(默认路由页面)", + "className": "component-base-style" + }, + "children": [], + "id": "622454f3" + } + ], + "dataSource": { + "list": [] + }, + "state": { + "textFivea": { + "text": "fivea" + } + }, + "methods": {}, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "FiveA", + "id": "body" + }, + "id": 9, + "isPage": true, + "group": "staticPages" + }, + "methods": {}, + "props": { + "className": "page-base-style" + }, + "state": { + "textFivea": { + "text": "fivea" + } + } + }, + { + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "This is page B", + "className": "component-base-style" + }, + "children": [], + "id": "4262d6e5" + } + ], + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "componentName": "Page", + "fileName": "FiveB", + "lifeCycles": {}, + "meta": { + "app": 1, + "gmt_create": "2025-09-18 17:53:38", + "lastUpdatedBy": "1", + "creator": "1", + "rootElement": false, + "contentBlocks": [], + "isHome": false, + "occupier": { + "id": "1", + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "siteId": "1", + "username": "开发者", + "email": "developer@lowcode.com", + "isAdmin": true, + "created_at": "2024-10-16 23:28:41", + "updated_at": "2024-10-16 23:28:41" + }, + "gmt_modified": "2025-09-18 17:53:38", + "parentId": "8", + "occupierBy": "1", + "isDefault": false, + "router": "fiveb", + "depth": 0, + "tenantId": "1", + "name": "FiveB", + "page_content": { + "componentName": "Page", + "css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", + "props": { + "className": "page-base-style" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "This is page B", + "className": "component-base-style" + }, + "children": [], + "id": "4262d6e5" + } + ], + "dataSource": { + "list": [] + }, + "state": {}, + "methods": {}, + "utils": [], + "bridge": [], + "inputs": [], + "outputs": [], + "fileName": "FiveB", + "id": "body" + }, + "id": 10, + "isPage": true, + "group": "staticPages" + }, + "methods": {}, + "props": { + "className": "page-base-style" + }, + "state": {} + } + ], + "config": { + "targetRootID": "app", + "sdkVersion": "1.0.3", + "historyMode": "hash" + }, + "constants": "", + "css": "", + "dataSource": { + "dataHandler": { + "type": "JSFunction", + "value": "function dataHanlder(res){\n return res;\n}" + }, + "list": [ + { + "id": 1, + "tenantId": "1", + "renterId": null, + "siteId": null, + "name": "tableTest1", + "data": { + "columns": [ + { + "name": "2", + "title": "姓名", + "field": "2", + "type": "string", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 10, + "dateTime": false + } + }, + { + "name": "1", + "title": "序号", + "field": "1", + "type": "number", + "format": { + "required": false, + "stringType": "", + "min": 0, + "max": 10, + "dateTime": false + } + } + ], + "data": [ + { + "1": 1, + "2": "张三", + "_id": "14553521" + } + ], + "type": "remote" + }, + "tpl": null, + "app": 1, + "platformId": 1, + "description": null, + "created_by": "1", + "last_updated_by": "1", + "created_at": "2025-08-31 21:25:54", + "updated_at": "2025-08-31 21:25:54" + }, + { + "id": 2, + "tenantId": "1", + "renterId": null, + "siteId": null, + "name": "localhost", + "data": { + "columns": [ + { + "name": "data", + "title": "", + "field": "data", + "type": "string", + "format": {} + }, + { + "name": "code", + "title": "", + "field": "code", + "type": "string", + "format": {} + }, + { + "name": "message", + "title": "", + "field": "message", + "type": "string", + "format": {} + }, + { + "name": "error", + "title": "", + "field": "error", + "type": "string", + "format": {} + }, + { + "name": "errMsg", + "title": "", + "field": "errMsg", + "type": "string", + "format": {} + }, + { + "name": "success", + "title": "", + "field": "success", + "type": "string", + "format": {} + } + ], + "data": [], + "type": "remote", + "options": { + "method": "GET", + "uri": "http://localhost:9090/app-center/v1/api/apps/schema/1" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(res){\n return res\n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option\n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {\n return Promise.reject(err)\n}" + } + }, + "tpl": null, + "app": 1, + "platformId": 1, + "description": null, + "created_by": "1", + "last_updated_by": "1", + "created_at": "2025-09-01 17:22:14", + "updated_at": "2025-09-01 17:22:14" + }, + { + "id": 3, + "tenantId": "1", + "renterId": null, + "siteId": null, + "name": "remoteTest", + "data": { + "columns": [ + { + "name": "data", + "title": "", + "field": "data", + "type": "string", + "format": {} + }, + { + "name": "code", + "title": "", + "field": "code", + "type": "number", + "format": {} + }, + { + "name": "message", + "title": "", + "field": "message", + "type": "string", + "format": {} + }, + { + "name": "error", + "title": "", + "field": "error", + "type": "string", + "format": {} + }, + { + "name": "errMsg", + "title": "", + "field": "errMsg", + "type": "string", + "format": {} + }, + { + "name": "success", + "title": "", + "field": "success", + "type": "string", + "format": {} + } + ], + "data": [ + { + "_id": "14b63435" + } + ], + "type": "remote", + "options": { + "method": "GET", + "uri": "http://localhost:9090/app-center/api/pages/detail/2" + }, + "dataHandler": { + "type": "JSFunction", + "value": "function dataHandler(res){\n return res\n}" + }, + "willFetch": { + "type": "JSFunction", + "value": "function willFetch(option) {\n return option\n}" + }, + "shouldFetch": { + "type": "JSFunction", + "value": "function shouldFetch(option) {\n return true \n}" + }, + "errorHandler": { + "type": "JSFunction", + "value": "function errorHandler(err) {\n return Promise.reject(err)\n}" + } + }, + "tpl": null, + "app": 1, + "platformId": 1, + "description": null, + "created_by": "1", + "last_updated_by": "1", + "created_at": "2025-09-01 17:41:03", + "updated_at": "2025-09-01 17:41:03" + } + ] + }, + "i18n": { + "en_US": { + "lowcode.26831252": "International" + }, + "zh_CN": { + "lowcode.26831252": "国际化" + } + }, + "meta": { + "appId": "1", + "branch": "develop", + "creator": "1", + "description": "demo应用", + "name": "portal-app", + "gitGroup": null, + "globalState": [ + { + "id": "test2", + "state": { + "name1": "xxx1" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + }, + { + "id": "test3", + "state": { + "name1": "xxx" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + }, + { + "id": "test4", + "state": { + "region": "", + "scenario": "all", + "productId": "", + "planId": "", + "addEvs": false, + "addHss": false, + "addCbr": false, + "period": { + "value": 1, + "unit": "month" + }, + "amount": 1 + }, + "getters": {}, + "actions": {} + }, + { + "id": "test1", + "state": { + "name1": "xxx" + }, + "getters": { + "count": { + "type": "JSFunction", + "value": "function count() {}" + } + }, + "actions": { + "actions": { + "type": "JSFunction", + "value": "function actions() {}" + } + } + }, + { + "id": "ttest1", + "state": { + "button": 1, + "text": "text", + "count": 1 + }, + "getters": { + "doubleButton": { + "type": "JSFunction", + "value": "function doubleButton() {\n return this.button * 2;\n}" + } + }, + "actions": { + "increcement": { + "type": "JSFunction", + "value": "function increcement() {\n console.log(this.button);\n this.button++;\n console.log('123456');\n}" + }, + "consoleTest": { + "type": "JSFunction", + "value": "function consoleTest() {\n this.button++;\n console.log('!!!!consoleTest!!!!!');\n}" + } + } + }, + { + "id": "ttest2", + "state": { + "button2": 20 + }, + "getters": {}, + "actions": {} + }, + { + "id": "ttest3", + "state": { + "cartItems": [ + { + "id": 1, + "name": "无线耳机", + "price": 999, + "quantity": 1, + "isDiscount": true + }, + { + "id": 2, + "name": "机械键盘", + "price": 499, + "quantity": 2, + "isDiscount": false + } + ], + "user": { + "isVIP": true + } + }, + "getters": {}, + "actions": {} + } + ], + "projectName": null, + "tenant": null, + "gmtCreate": "2024-10-16 23:27:10", + "gmtModified": "2024-10-16 23:27:10", + "isDemo": null + }, + "utils": [ + { + "name": "axios", + "type": "npm", + "content": { + "package": "axios", + "version": "", + "exportName": "axios", + "subName": "", + "destructuring": false, + "main": "", + "cdnLink": "https://unpkg.com/axios@1.0.0/dist/esm/axios.js" + } + }, + { + "name": "testFunction", + "type": "function", + "content": { + "type": "JSFunction", + "value": "function testFunction(){\r\n console.log(\"utils11111111\")\r\n}" + } + }, + { + "name": "uniq", + "type": "npm", + "content": { + "package": "lodash", + "version": "", + "exportName": "uniq", + "subName": "", + "destructuring": true, + "main": "", + "cdnLink": "https://esm.sh/lodash-es@4.17.21" + } + } + ], + "version": "", + "packages": [ + { + "name": "TinyVue组件库", + "version": "3.20.0", + "script": "https://registry.npmmirror.com/@opentiny/vue-runtime/~3.20/files/dist3/tiny-vue-pc.mjs", + "css": "https://registry.npmmirror.com/@opentiny/vue-theme/~3.20/files/index.css", + "others": {}, + "package": "@opentiny/vue" + }, + { + "name": "element-plus组件库", + "version": "2.4.2", + "script": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.full.mjs", + "css": "https://registry.npmmirror.com/element-plus/2.4.2/files/dist/index.css", + "others": {}, + "package": "element-plus" + } + ] + }, + "code": "200", + "message": "操作成功", + "error": null, + "errMsg": null, + "success": true +} diff --git a/packages/runtime-renderer/src/mock/blocks.json b/packages/runtime-renderer/src/mock/blocks.json new file mode 100644 index 0000000000..ee26846f94 --- /dev/null +++ b/packages/runtime-renderer/src/mock/blocks.json @@ -0,0 +1,380 @@ +{ + "data": [ + { + "id": 2, + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "renterId": null, + "siteId": null, + "label": "Group1Test1", + "framework": "Vue", + "content": { + "componentName": "Block", + "fileName": "Group1Test1", + "css": "", + "props": { + "id": "block1", + "firstname": "", + "lastname": "" + }, + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "className": "component-base-style", + "text": "测试:区块一" + }, + "children": [], + "id": "58231611" + } + ], + "id": "7331336a" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。", + "className": "component-base-style" + }, + "children": [], + "id": "52365445" + } + ], + "id": "3d234d26" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "Text", + "props": { + "style": "display: inline-block;", + "text": "state测试 :", + "className": "component-base-style" + }, + "children": [], + "id": "23633546" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.block1.lastname" + }, + "className": "component-base-style" + }, + "children": [], + "id": "65459355" + }, + { + "componentName": "TinyButton", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.block1.firstname" + }, + "className": "component-base-style" + }, + "children": [], + "id": "4346f115" + } + ], + "id": "e3652645" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "customProperty", + "type": "string", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "" + } + }, + "cols": 12, + "rules": [], + "accessor": { + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [begin, end] = this.state.block1.len.split(' ')\r\n this.emit('update:begin', begin)\r\n this.emit('update:end', end)\r\n}\r\n" + } + }, + "hidden": false, + "required": true, + "readOnly": false, + "disabled": false, + "widget": { + "component": "InputConfigurator", + "props": {} + }, + "properties": [ + { + "label": { + "zh_CN": "默认分组" + }, + "content": [] + } + ] + } + ] + } + ], + "events": {}, + "slots": {} + }, + "state": { + "block1": { + "defaultValue": { + "len": "begin end", + "firstname": "first-name", + "lastname": "last-name" + }, + "accessor": { + "setter": { + "type": "JSFunction", + "value": "function setter() {\r\n const [begin, end] = this.state.block1.len.split(' ')\r\n this.emit('update:begin', begin)\r\n this.emit('update:end', end)\r\n this.emit('update:firstname', this.firstname)\r\n this.emit('update:lastname', this.lastname)\r\n}" + } + } + } + }, + "methods": {}, + "dataSource": {}, + "dependencies": { + "scripts": [ + { + "package": "@opentiny/vue", + "components": { + "TinyButton": "Button" + } + } + ], + "styles": [] + }, + "id": "body" + }, + "assets": {}, + "description": null, + "tags": [], + "screenshot": null, + "path": null, + "i18n": {}, + "created_at": "2025-08-28 19:42:11", + "updated_at": "2025-08-28 19:42:11", + "name_cn": "test1", + "last_build_info": { + "result": true, + "versions": ["1.0.8"], + "endTime": "2025-08-29 17:57:00" + }, + "version": "1.0.8", + "current_history": 8, + "occupier": "1", + "is_official": false, + "public": 1, + "is_default": false, + "tiny_reserved": null, + "npm_name": null, + "platform_id": 1, + "created_app": 1, + "content_blocks": null, + "public_scope_tenants": [], + "histories_length": 0, + "is_published": true, + "current_version": null + }, + { + "id": 3, + "createdBy": "1", + "lastUpdatedBy": "1", + "tenantId": "1", + "renterId": null, + "siteId": null, + "label": "Group1Test2", + "framework": "Vue", + "content": { + "componentName": "Block", + "fileName": "Group1Test2", + "css": "", + "props": {}, + "children": [ + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "h3", + "props": { + "className": "component-base-style" + }, + "children": "区块2", + "id": "b656553d" + } + ], + "id": "42224532" + }, + { + "componentName": "div", + "props": { + "className": "component-base-style" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": "点击抛出错误", + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew" + } + }, + "children": [], + "id": "4142f242" + }, + { + "componentName": "TinyButton", + "props": { + "className": "component-base-style", + "text": { + "type": "JSExpression", + "value": "this.state.stateblock1.button" + }, + "onClick": { + "type": "JSExpression", + "value": "this.onClickNew1" + } + }, + "children": [], + "id": "593cc335" + } + ], + "id": "86332361" + } + ], + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [] + } + ], + "events": {}, + "slots": {} + }, + "state": { + "stateblock1": { + "button": 1 + } + }, + "methods": { + "onClickNew": { + "type": "JSFunction", + "value": "function onClickNew(event) {\n console.log('子组件按钮点击:数据请求失败(模拟)')\n throw new Error('子组件按钮点击:数据请求失败(模拟)')\n}\n" + }, + "onClickNew1": { + "type": "JSFunction", + "value": "function onClickNew1(event) {\n this.state.stateblock1.button++\n console.log(123456)\n}\n" + } + }, + "dataSource": {}, + "dependencies": { + "scripts": [ + { + "components": {} + }, + { + "package": "@opentiny/vue", + "components": { + "TinyButton": "Button" + } + } + ], + "styles": [] + }, + "id": "body" + }, + "assets": {}, + "description": null, + "tags": [], + "screenshot": null, + "path": null, + "i18n": {}, + "created_at": "2025-08-29 18:11:27", + "updated_at": "2025-08-29 18:11:27", + "name_cn": "test2", + "last_build_info": { + "result": true, + "versions": ["1.0.3"], + "endTime": "2025-09-03 17:00:23" + }, + "version": "1.0.3", + "current_history": 11, + "occupier": "1", + "is_official": false, + "public": 1, + "is_default": false, + "tiny_reserved": null, + "npm_name": null, + "platform_id": 1, + "created_app": 1, + "content_blocks": null, + "public_scope_tenants": [], + "histories_length": 0, + "is_published": true, + "current_version": null + } + ], + "code": "200", + "message": "操作成功", + "error": null, + "errMsg": null, + "success": true +} From 8a2db0b30adb93a4ee5b9b4d1d2ba4a9a0a48786 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sat, 4 Oct 2025 18:44:40 +0800 Subject: [PATCH 15/81] fix: update axios and vite dependencies --- packages/runtime-renderer/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json index 9dc4f67e77..bc70f51441 100644 --- a/packages/runtime-renderer/package.json +++ b/packages/runtime-renderer/package.json @@ -19,7 +19,7 @@ "@vue/babel-plugin-jsx": "^1.2.5", "@babel/core": "^7.18.13", "@vue/shared": "^3.3.4", - "axios": "latest", + "axios": "^1.10.0", "axios-mock-adapter": "^1.19.0", "element-plus": "2.4.2", "pinia": "^2.1.0", @@ -28,7 +28,7 @@ "devDependencies": { "@vitejs/plugin-vue": "^4.0.0", "typescript": "^5.9.2", - "vite": "^4.0.0" + "vite": "^5.4.2" }, "peerDependencies": { "@opentiny/vue": "^3.20.0", From 3dc95650670462718e67adc8ab4e0cbbf553a440 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 5 Oct 2025 14:25:20 +0800 Subject: [PATCH 16/81] fix: fix the async function issue --- designer-demo/src/runtime.js | 2 +- packages/common/js/runtime-deploy.js | 2 +- .../src/renderer/page-function/blockContext.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/designer-demo/src/runtime.js b/designer-demo/src/runtime.js index b6fc5c5bf6..243d95f02a 100644 --- a/designer-demo/src/runtime.js +++ b/designer-demo/src/runtime.js @@ -12,7 +12,7 @@ import { initRuntimeRenderer } from '@opentiny/tiny-engine-runtime-renderer' async function startApp() { - initRuntimeRenderer() + await initRuntimeRenderer() } startApp() diff --git a/packages/common/js/runtime-deploy.js b/packages/common/js/runtime-deploy.js index c755d414dd..e367bedc87 100644 --- a/packages/common/js/runtime-deploy.js +++ b/packages/common/js/runtime-deploy.js @@ -24,7 +24,7 @@ const getQueryParams = () => { return query } -export const deployPage = async () => { +export const deployPage = () => { const href = window.location.href.split('?')[0] || './' const query = getQueryParams() diff --git a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts index fecd93325b..3f723c0658 100644 --- a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts +++ b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts @@ -69,8 +69,8 @@ export const createBlockContext = () => { } } -export const getBlockContext = (schema: Schema) => { +export const getBlockContext = async (schema: Schema) => { const blockContext = createBlockContext() - blockContext.setSchema(schema) + await blockContext.setSchema(schema) return blockContext.getContext() } From 8c49f973668b8b47b09556daa4cd3e8b263f8647 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 5 Oct 2025 14:57:40 +0800 Subject: [PATCH 17/81] fix: fix package name usage in CDN URL construction --- packages/runtime-renderer/src/app-function/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime-renderer/src/app-function/utils.ts b/packages/runtime-renderer/src/app-function/utils.ts index b6e8281da9..ecd8487ecb 100644 --- a/packages/runtime-renderer/src/app-function/utils.ts +++ b/packages/runtime-renderer/src/app-function/utils.ts @@ -28,7 +28,8 @@ async function loadNpmUtil(util: Util) { const key = `${c.package}@${c.version || ''}` let mod = npmCache.get(key) if (!mod) { - const url = c.cdnLink || (c.version ? `https://unpkg.com/${pkg}@${c.version}` : `https://unpkg.com/${pkg}`) + const url = + c.cdnLink || (c.version ? `https://unpkg.com/${c.package}@${c.version}` : `https://unpkg.com/${c.package}`) mod = await import(/* @vite-ignore */ url) npmCache.set(key, mod) } From 9ef991bd6b6b91699924cc676403754f73a3a6fe Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 5 Oct 2025 20:02:17 +0800 Subject: [PATCH 18/81] fix: add error handling --- designer-demo/src/runtime.js | 8 +- packages/runtime-renderer/index.ts | 7 +- .../src/app-function/utils.ts | 9 +- .../src/composables/useAppSchema.ts | 83 ++++++++++++------- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/designer-demo/src/runtime.js b/designer-demo/src/runtime.js index 243d95f02a..89b947a911 100644 --- a/designer-demo/src/runtime.js +++ b/designer-demo/src/runtime.js @@ -12,7 +12,13 @@ import { initRuntimeRenderer } from '@opentiny/tiny-engine-runtime-renderer' async function startApp() { - await initRuntimeRenderer() + try { + await initRuntimeRenderer() + } + catch (error) { + //eslint-disable-next-line no-console + console.error('Failed to initialize runtime renderer:',error) + } } startApp() diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index 6a00d82136..20f81718e0 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -22,8 +22,13 @@ import i18n from '@opentiny/tiny-engine-i18n-host' export const initRuntimeRenderer = async () => { const searchParams = new URLSearchParams(location.search) const appId = searchParams.get('id') + + if (!appId) { + throw new Error('Missing required "id" query parameter') + } + const { fetchAppSchema, fetchBlocks } = useAppSchema() - await fetchAppSchema(appId || '') + await fetchAppSchema(appId) await fetchBlocks() const router = await createAppRouter() diff --git a/packages/runtime-renderer/src/app-function/utils.ts b/packages/runtime-renderer/src/app-function/utils.ts index ecd8487ecb..9afa494e42 100644 --- a/packages/runtime-renderer/src/app-function/utils.ts +++ b/packages/runtime-renderer/src/app-function/utils.ts @@ -58,8 +58,13 @@ export async function initUtils(utils: Util[] = []) { console.error(`加载 npm 包 ${util.name} 失败:`, error) } } else if (util.type === 'function') { - const content = util.content as fnContent - utilValues.set(util.name, parseJSFunction(content)) + try { + const content = util.content as fnContent + utilValues.set(util.name, parseJSFunction(content)) + } catch (error) { + // eslint-disable-next-line no-console + console.error(`加载函数 ${util.name} 错误:`, error) + } } } initialized = true diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index 92916638bd..713ca80cc4 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -41,7 +41,7 @@ export function useAppSchema() { initializeI18n(schema.data.i18n) // 初始化工具函数 - initializeUtils(schema.data.utils) + await initializeUtils(schema.data.utils) // 注入全局CSS injectGlobalCSS(schema.data.css) @@ -54,8 +54,18 @@ export function useAppSchema() { try { const response = await fetch(`/app-center/v1/api/apps/schema/${appId}`) - appSchema.value = await response.json() + if (!response.ok) { + throw new Error(`加载应用Schema失败: HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (!data?.data) { + throw new Error('应用Schema数据无效') + } + + appSchema.value = data // 解析并初始化应用级配置 await initializeAppConfig(appSchema.value) } catch (err) { @@ -69,38 +79,53 @@ export function useAppSchema() { // 拉取区块schema const fetchBlocks = async () => { - const response = await fetch('/material-center/api/blocks') - const blockJSON = await response.json() - const blocks: BlockItem[] = blockJSON.data || [] - - // 转换为组件映射格式 - const blocksMap: Record< - string, - { - schema: BlockContent - meta: { - id: number - label: string - framework: string - version: string - } + try { + const response = await fetch('/material-center/api/blocks') + + if (!response.ok) { + throw new Error(`加载区块Schema失败: HTTP ${response.status}: ${response.statusText}`) + } + + const blockJSON = await response.json() + + if (!Array.isArray(blockJSON?.data)) { + throw new Error('区块Schema数据无效') } - > = {} - blocks.forEach((block) => { - if (block.content) { - blocksMap[block.label] = { - schema: block.content, + + const blocks: BlockItem[] = blockJSON.data || [] + + // 转换为组件映射格式 + const blocksMap: Record< + string, + { + schema: BlockContent meta: { - id: block.id, - label: block.label, - framework: block.framework, - version: block.version + id: number + label: string + framework: string + version: string } } - } - }) + > = {} + blocks.forEach((block) => { + if (block.content) { + blocksMap[block.label] = { + schema: block.content, + meta: { + id: block.id, + label: block.label, + framework: block.framework, + version: block.version + } + } + } + }) - window.blocks = blocksMap + window.blocks = blocksMap + } catch (error) { + // eslint-disable-next-line no-console + console.error('加载区块Schema失败:', error) + } } // 获取页面列表 From 57ca2bb403875c5150f27f7948d0716942b6e157 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Mon, 6 Oct 2025 16:02:12 +0800 Subject: [PATCH 19/81] fix: move app.provide call before app.use chain --- packages/runtime-renderer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index 20f81718e0..ca204df5ae 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -37,8 +37,8 @@ export const initRuntimeRenderer = async () => { const stores = createStores(storesConfig, pinia) const app = createApp(App) + app.provide('stores', stores) app.use(pinia).use(router).use(i18n).mount('#app') - app.provide('stores', stores) return app } From 4b9f2087efb093a5f83d551f31bf343fde79527f Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Mon, 6 Oct 2025 16:28:52 +0800 Subject: [PATCH 20/81] fix: improve query parameter handling in deploy script --- packages/common/js/runtime-deploy.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/common/js/runtime-deploy.js b/packages/common/js/runtime-deploy.js index e367bedc87..674a864887 100644 --- a/packages/common/js/runtime-deploy.js +++ b/packages/common/js/runtime-deploy.js @@ -20,8 +20,12 @@ const getQueryParams = () => { const platform = getMergeMeta('engine.config')?.platformId const appId = paramsMap.get('id') - let query = `id=${appId}&tenant=${tenant}&platform=${platform}` - return query + const params = new URLSearchParams() + if (appId) params.set('id', appId) + if (tenant) params.set('tenant', tenant) + if (platform) params.set('platform', platform) + + return params.toString() } export const deployPage = () => { @@ -34,7 +38,7 @@ export const deployPage = () => { let openUrl = '' openUrl = customDeployUrl ? typeof customDeployUrl === 'function' - ? customDeployUrl(defaultDeployUrl, query) + ? String(customDeployUrl(defaultDeployUrl, query) || defaultDeployUrl) : `${customDeployUrl}?${query}` : `${defaultDeployUrl}?${query}` From d93124c6a8971625344333126525433a3abe9d1c Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 7 Oct 2025 01:07:23 +0800 Subject: [PATCH 21/81] fix: change getBlockContext to synchronous function --- .../src/renderer/page-function/blockContext.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts index 3f723c0658..780283cb6a 100644 --- a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts +++ b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts @@ -69,8 +69,9 @@ export const createBlockContext = () => { } } -export const getBlockContext = async (schema: Schema) => { +// 暂时不写成异步函数形式,方便后续调用 +export const getBlockContext = (schema: Schema) => { const blockContext = createBlockContext() - await blockContext.setSchema(schema) + blockContext.setSchema(schema) return blockContext.getContext() } From c44e1879015d28b7abfb151705d3c46c022a52ad Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 7 Oct 2025 14:59:31 +0800 Subject: [PATCH 22/81] fix: remove unused styles and comments --- .../src/components/NotFound.vue | 47 ------------------- .../src/renderer/useContext.ts | 2 - 2 files changed, 49 deletions(-) diff --git a/packages/runtime-renderer/src/components/NotFound.vue b/packages/runtime-renderer/src/components/NotFound.vue index 62af36799d..fa8b95981c 100644 --- a/packages/runtime-renderer/src/components/NotFound.vue +++ b/packages/runtime-renderer/src/components/NotFound.vue @@ -140,43 +140,6 @@ const refresh = () => { transform: translateY(-2px); } -.available-pages { - margin-top: 40px; - padding-top: 30px; - border-top: 1px solid #eee; -} - -.available-pages h3 { - color: #333; - margin-bottom: 20px; - font-size: 1.2rem; - font-weight: 500; -} - -.page-list { - display: flex; - flex-wrap: wrap; - gap: 10px; - justify-content: center; -} - -.page-link { - background: #f8f9fa; - color: #495057; - border: 1px solid #dee2e6; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - font-size: 0.9rem; -} - -.page-link:hover { - background: #e9ecef; - border-color: #adb5bd; - transform: translateY(-1px); -} - /* 响应式设计 */ @media (max-width: 768px) { .not-found-content { @@ -196,15 +159,5 @@ const refresh = () => { width: 100%; max-width: 200px; } - - .page-list { - flex-direction: column; - align-items: center; - } - - .page-link { - width: 100%; - max-width: 200px; - } } diff --git a/packages/runtime-renderer/src/renderer/useContext.ts b/packages/runtime-renderer/src/renderer/useContext.ts index 20f7ce9e8c..1db4ece666 100644 --- a/packages/runtime-renderer/src/renderer/useContext.ts +++ b/packages/runtime-renderer/src/renderer/useContext.ts @@ -25,8 +25,6 @@ interface UseContextReturn { export default (): UseContextReturn => { const context = shallowReactive({}) - // 从大纲树控制隐藏 - const setContext = (ctx: Context, clear?: boolean) => { if (clear) { Object.keys(context).forEach((key) => delete context[key]) From 325e9d8f55b845bc288c6e90b2d9c173e5f8e548 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 7 Oct 2025 16:40:28 +0800 Subject: [PATCH 23/81] fix: add error logging for missing parent routes --- packages/runtime-renderer/src/router/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/router/index.ts index 0704d30858..9b1d9a99a5 100644 --- a/packages/runtime-renderer/src/router/index.ts +++ b/packages/runtime-renderer/src/router/index.ts @@ -36,7 +36,7 @@ async function createRouterConfig() { } if (isChildRoute) { - const parentId = parseInt(page.meta.parentId) + const parentId = parseInt(page.meta.parentId, 10) const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId) if (parentRoute) { parentRoute.children = parentRoute.children || [] @@ -48,6 +48,11 @@ async function createRouterConfig() { parentRoute.redirect = parentRoute.meta.defaultPath } return + } else { + // eslint-disable-next-line no-console + console.error( + `父路由未找到: 页面 "${page.meta.name}" (ID: ${page.meta.id}) 引用的父路由 ID ${parentId} 不存在` + ) } } else { routesConfig.push(routeConfigCurrent) From 23bed273cf19f636768cf6d52b87d09c708c46ac Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Thu, 9 Oct 2025 17:25:21 +0800 Subject: [PATCH 24/81] fix: improve schema handling --- packages/runtime-renderer/src/renderer/RenderMain.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 5538fd3179..10b983641c 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -39,8 +39,10 @@ export default defineComponent({ const { getPageById } = useAppSchema() const currentSchema = computed(() => { - const page = getPageById(props.pageId)?.meta?.page_content // 通过 pageId 获取最新的页面对象 - return JSON.parse(JSON.stringify(page)) + const page = getPageById(props.pageId) // 通过 pageId 获取最新的页面对象 + const pageContent = page?.meta?.page_content + if (!pageContent) return null + return JSON.parse(JSON.stringify(pageContent)) }) const route = useRoute() @@ -102,9 +104,9 @@ export default defineComponent({ // 监听 schema 变化 watch( () => currentSchema.value, - async () => { - const schema = currentSchema.value - if (!schema || !Object.keys(schema).length) return + async (schema) => { + if (!schema) return + if (Object.keys(schema).length === 0) return await setSchema(schema) }, { immediate: true } From ebfb795b86136621de7f1c26d206c61a6a923253 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Thu, 9 Oct 2025 17:35:35 +0800 Subject: [PATCH 25/81] fix: handle orphaned child routes as top-level routes --- packages/runtime-renderer/src/router/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/router/index.ts index 9b1d9a99a5..462e5817aa 100644 --- a/packages/runtime-renderer/src/router/index.ts +++ b/packages/runtime-renderer/src/router/index.ts @@ -53,6 +53,9 @@ async function createRouterConfig() { console.error( `父路由未找到: 页面 "${page.meta.name}" (ID: ${page.meta.id}) 引用的父路由 ID ${parentId} 不存在` ) + // 将孤立的子路由作为顶级路由添加,确保其仍可访问 + routeConfigCurrent.path = `/${page.meta.router}` + routesConfig.push(routeConfigCurrent) } } else { routesConfig.push(routeConfigCurrent) From 515191b28a49fcef95a25860164ef827ff1dead1 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Thu, 9 Oct 2025 17:36:16 +0800 Subject: [PATCH 26/81] fix: add global error handler to runtime renderer --- packages/runtime-renderer/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index ca204df5ae..6c15fdb03b 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -38,6 +38,17 @@ export const initRuntimeRenderer = async () => { const app = createApp(App) app.provide('stores', stores) + + // 全局错误处理(防止 scheduler 被打断) + app.config.errorHandler = (err, instance, info) => { + // eslint-disable-next-line no-console + console.error('[GlobalErrorHandler]', err, info) + if ((err as any)?.stack) { + // eslint-disable-next-line no-console + console.error('[GlobalErrorHandler stack]', (err as any).stack) + } + } + app.use(pinia).use(router).use(i18n).mount('#app') return app From e23c648c01ab598e8e8574b1f34703da5601841d Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 12 Oct 2025 11:04:26 +0800 Subject: [PATCH 27/81] feat: improve native component loading --- packages/runtime-renderer/package.json | 3 +- .../src/app-function/nativeComponents.ts | 128 ++++++++++++++++++ .../src/composables/useAppSchema.ts | 37 ++++- .../runtime-renderer/src/renderer/render.ts | 13 +- 4 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 packages/runtime-renderer/src/app-function/nativeComponents.ts diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json index bc70f51441..71d9dd817c 100644 --- a/packages/runtime-renderer/package.json +++ b/packages/runtime-renderer/package.json @@ -21,7 +21,6 @@ "@vue/shared": "^3.3.4", "axios": "^1.10.0", "axios-mock-adapter": "^1.19.0", - "element-plus": "2.4.2", "pinia": "^2.1.0", "vue-router": "^4.2.0" }, @@ -33,6 +32,8 @@ "peerDependencies": { "@opentiny/vue": "^3.20.0", "@opentiny/vue-icon": "^3.20.0", + "@opentiny/vue-locale": "~3.20.0", + "@opentiny/vue-theme": "~3.20.0", "vue": "^3.4.15", "vue-i18n": "^9.9.0" } diff --git a/packages/runtime-renderer/src/app-function/nativeComponents.ts b/packages/runtime-renderer/src/app-function/nativeComponents.ts new file mode 100644 index 0000000000..d8dec609b8 --- /dev/null +++ b/packages/runtime-renderer/src/app-function/nativeComponents.ts @@ -0,0 +1,128 @@ +// 定义全局类型声明 +declare global { + interface Window { + TinyComponentLibs: Record + TinyLowcodeComponent: Record + } +} + +// 定义组件配置接口 +interface ComponentConfig { + destructuring?: boolean + exportName?: string +} + +// 定义组件依赖接口 +interface ComponentDependency { + package?: string | null + components: Record + npmrc?: any +} + +// 定义动态导入参数接口 +interface DynamicImportParams { + package: string + script?: string +} + +export const addStyle = (href: string, doc = document): Promise => { + return new Promise((resolve, reject) => { + const link = doc.createElement('link') + + link.setAttribute('href', href) + link.setAttribute('rel', 'stylesheet') + + link.onload = resolve + link.onerror = reject + + doc.querySelector('head')!.appendChild(link) + }) +} + +/** + * 动态导入获取组件库模块 + * @param {DynamicImportParams} param 模块参数,包含pkg模块名称和script模块的cdn地址 + * @returns {Promise} 返回组件库模块 + */ +const dynamicImportComponentLib = async ({ package: pkg, script }: DynamicImportParams): Promise => { + if (window.TinyComponentLibs[pkg]) { + return window.TinyComponentLibs[pkg] + } + + if (!script) { + return {} + } + + const href = window.location.href + const scriptUrl = script.startsWith('.') ? new URL(script, href).href : script + + try { + if (!window.TinyComponentLibs[pkg]) { + const modules = await import(/* @vite-ignore */ scriptUrl) + + window.TinyComponentLibs[pkg] = modules + } + } catch (error) { + // eslint-disable-next-line no-console + console.log(`加载组件库失败: ${pkg}`, error) + } + + return window.TinyComponentLibs[pkg] +} + +/** + * 获取组件库的package依赖 + * @param {DynamicImportParams[]} packageDependencys 组件库的package依赖数组 + * @returns {Promise} 返回组件库的package依赖对象 + */ +export const loadPackageDependencys = async (packageDependencys: DynamicImportParams[]): Promise => { + for (const packageDependency of packageDependencys) { + const { package: pkg, script } = packageDependency + if (pkg === '@opentiny/vue') continue + await dynamicImportComponentLib({ package: pkg, script }) + } +} + +export const getComponentLibs = async (pkg: string, npmrc?: string) => { + if (window.TinyComponentLibs[pkg]) { + return window.TinyComponentLibs[pkg] + } else { + // 如果组件包含npmrc字段,则尝试从npmrc中引入模块 + if (npmrc && npmrc !== 'null' && npmrc !== '') { + try { + const modules = await import(/* @vite-ignore */ npmrc) + window.TinyComponentLibs[pkg] = modules + return modules + } catch (error) { + // eslint-disable-next-line no-console + console.error(`从 npmrc 加载组件库失败: ${pkg}`, error) + } + } + throw new Error(`${pkg} 组件库未找到`) + } +} + +/** + * 获取组件对象并缓存,组件渲染时使用 + * @param {ComponentDependency} param 组件的依赖配置对象 + * @returns {Promise} 无返回值的Promise + */ +export const getComponents = async ({ package: pkg, components, npmrc }: ComponentDependency): Promise => { + if (!pkg || pkg === '@opentiny/vue') return + + const modules = await getComponentLibs(pkg, npmrc) + + Object.entries(components).forEach(([componentId, item]) => { + if (!window.TinyLowcodeComponent[componentId]) { + // 兼容老版本 - 当item是字符串时,直接作为模块导出名使用 + if (typeof item === 'string') { + window.TinyLowcodeComponent[componentId] = modules[item] + } else { + // 当item是配置对象时,根据destructuring属性决定如何获取组件 + const config = item as ComponentConfig + window.TinyLowcodeComponent[componentId] = + config?.destructuring && config?.exportName ? modules[config.exportName] : modules?.default + } + } + }) +} diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index 713ca80cc4..25d41cd8db 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -1,12 +1,42 @@ import { ref, computed, readonly } from 'vue' -import type { AppSchema, Util, BlockItem, BlockContent, I18nConfig } from '../types/schema' +import type { AppSchema, Util, BlockItem, BlockContent, I18nConfig, ComponentMap, PackageConfig } from '../types/schema' import { initUtils } from '../app-function/utils' import i18n from '@opentiny/tiny-engine-i18n-host' +import { addStyle, getComponents, loadPackageDependencys } from '../app-function/nativeComponents' const appSchema = ref(null) const isLoading = ref(false) const error = ref(null) +window.TinyLowcodeComponent = {} +window.TinyComponentLibs = {} + export function useAppSchema() { + const initializeComponentsMap = async (componentsMap: ComponentMap[], packages: PackageConfig[]) => { + // 获取组件依赖 + const componentsDeps = componentsMap.map((component: ComponentMap) => ({ + package: component.package, + components: { + [component.componentName]: component.destructuring + ? { destructuring: true, exportName: component.exportName || component.componentName } + : component.componentName + }, + npmrc: component.npmrc + })) + + await loadPackageDependencys(packages) + + // 获取包依赖中的样式 + const styles = packages.map((pkg) => pkg.css).filter((css) => css) as string[] + + try { + // 并行加载所有组件依赖和包资源,与 runner.ts 中的机制保持一致 + await Promise.all([...componentsDeps.map(getComponents), ...styles.map((src) => addStyle(src))]) + } catch (error) { + // eslint-disable-next-line no-console + console.error('组件或资源加载失败:', error) + } + } + // 初始化工具函数 const initializeUtils = async (utils: Util[]) => { try { @@ -37,6 +67,9 @@ export function useAppSchema() { const initializeAppConfig = async (schema: AppSchema) => { if (!schema?.data) return + // 初始化除tinyVue之外的nativeComponents + initializeComponentsMap(schema.data.componentsMap, schema.data.packages) + // 初始化国际化 initializeI18n(schema.data.i18n) @@ -67,7 +100,7 @@ export function useAppSchema() { appSchema.value = data // 解析并初始化应用级配置 - await initializeAppConfig(appSchema.value) + await initializeAppConfig(appSchema.value!) } catch (err) { error.value = err instanceof Error ? err.message : '加载应用Schema失败' // eslint-disable-next-line no-console diff --git a/packages/runtime-renderer/src/renderer/render.ts b/packages/runtime-renderer/src/renderer/render.ts index 0a46927fc2..d05d8b6a70 100644 --- a/packages/runtime-renderer/src/renderer/render.ts +++ b/packages/runtime-renderer/src/renderer/render.ts @@ -21,8 +21,6 @@ import { CanvasFlexBox, CanvasSection } from '@opentiny/tiny-engine-builtin-component' -import { ElInput, ElDatePicker, ElButton, ElForm, ElFormItem, ElTable, ElTableColumn } from 'element-plus' -import 'element-plus/dist/index.css' import { CanvasBox, CanvasIcon, @@ -55,20 +53,13 @@ const Mapper = { CanvasPlaceholder, RouterLink: CanvasRouterLink, RouterView: CanvasRouterView, - Collection: CanvasCollection, - ElInput, - ElDatePicker, - ElButton, - ElForm, - ElFormItem, - ElTable, - ElTableColumn + Collection: CanvasCollection } export const collectionMethodsMap = {} const getNative = (name) => { - return TinyVue?.[name] + return TinyVue?.[name] || window.TinyLowcodeComponent?.[name] } const getBlock = (name) => { From 6f9eec4fdb1540a7d732e0be84e0f9059e7c1d78 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 12 Oct 2025 12:18:56 +0800 Subject: [PATCH 28/81] feat: improve initUtils to parallelize npm util loading --- .../src/app-function/utils.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/runtime-renderer/src/app-function/utils.ts b/packages/runtime-renderer/src/app-function/utils.ts index 9afa494e42..efac49418e 100644 --- a/packages/runtime-renderer/src/app-function/utils.ts +++ b/packages/runtime-renderer/src/app-function/utils.ts @@ -48,16 +48,24 @@ export async function initUtils(utils: Util[] = []) { return } loading = true + try { + const npmUtils = utils.filter((util) => util.type === 'npm') + const functionUtils = utils.filter((util) => util.type === 'function') - for (const util of utils) { - if (util.type === 'npm') { - try { - await loadNpmUtil(util) - } catch (error) { - // eslint-disable-next-line no-console - console.error(`加载 npm 包 ${util.name} 失败:`, error) - } - } else if (util.type === 'function') { + // 并行加载npm包 + await Promise.all( + npmUtils.map(async (util) => { + try { + await loadNpmUtil(util) + } catch (error) { + // eslint-disable-next-line no-console + console.error(`加载 npm 包 ${util.name} 错误:`, error) + } + }) + ) + + // 处理funtion类型的utils + for (const util of functionUtils) { try { const content = util.content as fnContent utilValues.set(util.name, parseJSFunction(content)) @@ -66,9 +74,10 @@ export async function initUtils(utils: Util[] = []) { console.error(`加载函数 ${util.name} 错误:`, error) } } + } finally { + initialized = true + loading = false } - initialized = true - loading = false } export function getUtilsAll() { From 62c1efd4dec1d66a4f100aa993903ada79785d07 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 12 Oct 2025 13:24:15 +0800 Subject: [PATCH 29/81] feat: add global Window type declarations in types.d.ts --- .../runtime-renderer/src/app-function/nativeComponents.ts | 8 -------- packages/runtime-renderer/types.d.ts | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 packages/runtime-renderer/types.d.ts diff --git a/packages/runtime-renderer/src/app-function/nativeComponents.ts b/packages/runtime-renderer/src/app-function/nativeComponents.ts index d8dec609b8..7449cfc1d3 100644 --- a/packages/runtime-renderer/src/app-function/nativeComponents.ts +++ b/packages/runtime-renderer/src/app-function/nativeComponents.ts @@ -1,11 +1,3 @@ -// 定义全局类型声明 -declare global { - interface Window { - TinyComponentLibs: Record - TinyLowcodeComponent: Record - } -} - // 定义组件配置接口 interface ComponentConfig { destructuring?: boolean diff --git a/packages/runtime-renderer/types.d.ts b/packages/runtime-renderer/types.d.ts new file mode 100644 index 0000000000..0aea3f3335 --- /dev/null +++ b/packages/runtime-renderer/types.d.ts @@ -0,0 +1,7 @@ +export declare global { + interface Window { + TinyLowcodeComponent: Record + TinyComponentLibs: Record + blocks: Record + } +} From f7ee10f590d3c579aa21b6c852e155f295752ae3 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 12 Oct 2025 14:58:08 +0800 Subject: [PATCH 30/81] fix: add error notification for failed runtime window open --- packages/common/js/runtime-deploy.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/common/js/runtime-deploy.js b/packages/common/js/runtime-deploy.js index 674a864887..b043f7aa74 100644 --- a/packages/common/js/runtime-deploy.js +++ b/packages/common/js/runtime-deploy.js @@ -10,7 +10,7 @@ * */ -import { getMergeMeta } from '@opentiny/tiny-engine-meta-register' +import { getMergeMeta, useNotify } from '@opentiny/tiny-engine-meta-register' import { isDevelopEnv } from './environments' let runtimeWindow = null @@ -59,4 +59,11 @@ export const runtimeDeploy = async () => { } runtimeWindow = window.open(openUrl, 'tiny-engine-runtime') + if (!runtimeWindow) { + useNotify({ + type: 'error', + title: '运行时窗口打开失败', + message: '请检查浏览器是否允许新窗口打开' + }) + } } From bafb6ece9fce89c4ef2154c31a7e7323fe5eeac9 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 19 Oct 2025 23:36:23 +0800 Subject: [PATCH 31/81] fix: refactor PageRenderer from Vue SFC to TypeScript HOC --- .../src/components/PageRenderer.ts | 27 +++++++++++++++++++ .../src/components/PageRenderer.vue | 9 ------- packages/runtime-renderer/src/router/index.ts | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 packages/runtime-renderer/src/components/PageRenderer.ts delete mode 100644 packages/runtime-renderer/src/components/PageRenderer.vue diff --git a/packages/runtime-renderer/src/components/PageRenderer.ts b/packages/runtime-renderer/src/components/PageRenderer.ts new file mode 100644 index 0000000000..688260d40d --- /dev/null +++ b/packages/runtime-renderer/src/components/PageRenderer.ts @@ -0,0 +1,27 @@ +import { defineComponent, h } from 'vue' +import RenderMain from '../renderer/RenderMain' + +export function withPageRenderer(WrappedComponent: any) { + return defineComponent({ + name: 'PageRendererHOC', + props: { + pageId: { + type: Number, + required: true + } + }, + setup(props) { + return () => { + return h(WrappedComponent, { + pageId: props.pageId, + // ref: pageInstance, + key: props.pageId + }) + } + } + }) +} + +// 默认导出 +const PageRendererHOC = withPageRenderer(RenderMain) +export default PageRendererHOC diff --git a/packages/runtime-renderer/src/components/PageRenderer.vue b/packages/runtime-renderer/src/components/PageRenderer.vue deleted file mode 100644 index fb65f3118a..0000000000 --- a/packages/runtime-renderer/src/components/PageRenderer.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/router/index.ts index 462e5817aa..2a3ab7f74f 100644 --- a/packages/runtime-renderer/src/router/index.ts +++ b/packages/runtime-renderer/src/router/index.ts @@ -19,7 +19,7 @@ async function createRouterConfig() { const routeConfigCurrent = { path: isChildRoute ? page.meta.router : `/${page.meta.router}`, name: `${page.meta.id}`, - component: () => import('../components/PageRenderer.vue'), // 懒加载,避免过早引入RenderMain + component: async () => (await import('../components/PageRenderer.ts')).default, // 懒加载,避免过早引入RenderMain props: { pageId: page.meta.id }, // 静态对象,避免路由嵌套时被覆盖 children: [], meta: { From 61d5fa0fd6971f12bac5ba964a2beda69b73ead1 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Sun, 19 Oct 2025 23:58:41 +0800 Subject: [PATCH 32/81] fix: move lifecycle handling into renderer component --- .../src/renderer/LifecycleWrapper.ts | 208 ------------------ .../src/renderer/RenderMain.ts | 4 +- .../runtime-renderer/src/renderer/render.ts | 113 +++++++++- 3 files changed, 114 insertions(+), 211 deletions(-) delete mode 100644 packages/runtime-renderer/src/renderer/LifecycleWrapper.ts diff --git a/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts b/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts deleted file mode 100644 index 3e51a352f7..0000000000 --- a/packages/runtime-renderer/src/renderer/LifecycleWrapper.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { - defineComponent, - inject, - onBeforeMount, - onMounted, - onBeforeUpdate, - onUpdated, - onBeforeUnmount, - onUnmounted, - onErrorCaptured, - onActivated, - onDeactivated, - watchEffect, - ref, - h, - type PropType -} from 'vue' -import { Notify } from '@opentiny/vue' -import renderer from './render' -import { parseData } from './parser' - -interface JSFunction { - type: 'JSFunction' - value: string -} - -// 执行用户定义的生命周期函数 -const executeUserLifecycle = (hookName: string, lifeCycleConfig: JSFunction | undefined, context: any) => { - if (!lifeCycleConfig || lifeCycleConfig.type !== 'JSFunction') { - return - } - - try { - const fn = parseData(lifeCycleConfig, {}, context) - if (typeof fn === 'function') { - fn.call(context, context) - } - } catch (error) { - Notify({ - type: 'warning', - title: `${hookName} 生命周期执行失败`, - message: (error as any)?.message || `${hookName} 生命周期函数执行报错,请检查语法` - }) - } -} - -// 页面级生命周期包裹器 -export const PageLifecycleWrapper = defineComponent({ - name: 'PageLifecycleWrapper', - props: { - schema: { - type: Object as PropType, - required: true - }, - parent: { - type: Object, - default: () => ({}) - } - }, - setup(props) { - const lifeCycles = props.parent.lifeCycles - const pageContext = inject('pageContext') as any - let isInitialized = false - - // setup 生命周期 - 在组件创建时立即执行 - if (lifeCycles?.setup) { - executeUserLifecycle('setup', lifeCycles?.setup, pageContext) - } - - // 创建响应式状态,用于监听变化 - const reactiveState = ref({ - schema: props.schema, - timestamp: Date.now(), - // 添加页面状态变化的追踪 - stateSnapshot: null as any - }) - - // 监听页面状态变化,触发更新生命周期 - watchEffect(() => { - if (!isInitialized) { - return - } - - // 获取页面上下文的最新状态 - const pageContextData = pageContext.getContext() - - // 监听页面上下文中的响应式数据 - const { state, stores } = pageContextData - - // 监听状态变化, 建立响应式依赖 - if (state) { - // 访问 state 的各个属性,建立响应式依赖 - Object.keys(state).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - state[key] // 访问会建立响应式依赖 - }) - } - - // 监听 stores 变化,建立响应式依赖 - if (stores) { - Object.keys(stores).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - stores[key] // 这里访问会建立响应式依赖 - }) - } - - // 监听数据源变化 - 建立响应式依赖 - const dataSourceMap = pageContextData.dataSourceMap - if (dataSourceMap) { - Object.keys(dataSourceMap).forEach((key) => { - const dataSource = dataSourceMap[key] - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - dataSource // 这里访问会建立响应式依赖 - }) - } - - // 创建状态快照,用于追踪变化 - const stateSnapshot = { - state: state ? JSON.stringify(state) : null, - stores: stores ? JSON.stringify(stores) : null, - dataSourceMap: dataSourceMap ? JSON.stringify(dataSourceMap) : null - } - - // 更新响应式状态,这会触发组件重新渲染 - const newTimestamp = Date.now() - reactiveState.value = { - schema: props.schema, - timestamp: newTimestamp, - stateSnapshot - } - }) - - // onBeforeMount 生命周期 - onBeforeMount(() => { - executeUserLifecycle('onBeforeMount', lifeCycles?.onBeforeMount, pageContext) - }) - - // onMounted 生命周期 - onMounted(() => { - executeUserLifecycle('onMounted', lifeCycles?.onMounted, pageContext) - isInitialized = true // 标记为已初始化,允许更新生命周期执行 - }) - - // onBeforeUpdate 生命周期 - onBeforeUpdate(() => { - if (isInitialized) { - executeUserLifecycle('onBeforeUpdate', lifeCycles?.onBeforeUpdate, pageContext) - } - }) - - // onUpdated 生命周期 - onUpdated(() => { - if (isInitialized) { - executeUserLifecycle('onUpdated', lifeCycles?.onUpdated, pageContext) - } - }) - - // onBeforeUnmount 生命周期 - onBeforeUnmount(() => { - executeUserLifecycle('onBeforeUnmount', lifeCycles?.onBeforeUnmount, pageContext) - isInitialized = false // 重置初始化状态 - }) - - // onUnmounted 生命周期 - onUnmounted(() => { - executeUserLifecycle('onUnmounted', lifeCycles?.onUnmounted, pageContext) - }) - - // onErrorCaptured 生命周期 - onErrorCaptured((error, instance, info) => { - if (lifeCycles?.onErrorCaptured) { - try { - const fn = parseData(lifeCycles?.onErrorCaptured, {}, pageContext) - if (typeof fn === 'function') { - // 将错误信息传递给用户函数 - const result = fn.call(pageContext, error, instance, info) - // 如果用户函数返回false,阻止错误继续传播 - return result === false - } - } catch (userError) { - Notify({ - type: 'warning', - title: 'onErrorCaptured 生命周期执行失败', - message: (userError as any)?.message || 'onErrorCaptured 生命周期函数执行报错,请检查语法' - }) - } - } - // 默认让错误继续传播 - return true - }) - - // onActivated 生命周期 (keep-alive 组件激活时) - onActivated(() => { - executeUserLifecycle('onActivated', lifeCycles?.onActivated, pageContext) - }) - - // onDeactivated 生命周期 (keep-alive 组件失活时) - onDeactivated(() => { - executeUserLifecycle('onDeactivated', lifeCycles?.onDeactivated, pageContext) - }) - - return () => - h(renderer, { - schema: reactiveState.value.schema, - parent: props.parent - }) - } -}) diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 10b983641c..ef95a609f0 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -13,7 +13,7 @@ import { h, computed, provide, nextTick, reactive, watch, defineComponent, inject } from 'vue' import Loading from '../components/Loading.vue' import { parseData } from './parser/parser.ts' -import { PageLifecycleWrapper } from './LifecycleWrapper.ts' +import { renderer } from './render.ts' import { setPageCss } from './page-function/css-handler.ts' import { useState } from './page-function/state' import useContext from './useContext.ts' @@ -129,7 +129,7 @@ export default defineComponent({ } return this.pageSchema.children?.length - ? h(PageLifecycleWrapper, { schema: rootChildrenSchema, parent: this.pageSchema }) + ? h(renderer, { schema: rootChildrenSchema, parent: this.pageSchema }) : [h(Loading)] } }) diff --git a/packages/runtime-renderer/src/renderer/render.ts b/packages/runtime-renderer/src/renderer/render.ts index d05d8b6a70..96c150a7f4 100644 --- a/packages/runtime-renderer/src/renderer/render.ts +++ b/packages/runtime-renderer/src/renderer/render.ts @@ -10,7 +10,22 @@ * */ -import { h, provide, inject, defineComponent } from 'vue' +import { + h, + provide, + inject, + defineComponent, + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, + onDeactivated +} from 'vue' +import { Notify } from '@opentiny/vue' import { isHTMLTag, hyphenate } from '@vue/shared' import TinyVue from '@opentiny/vue' import { getBlockContext } from './page-function/blockContext' @@ -316,6 +331,7 @@ const getChildren = (schema, mergeScope, context, renderComponent) => { function renderComponent(schema, scope, parent) { const { componentName, loop, loopArgs, condition } = schema + //console.log('renderComponent', schema) // 处理数据源和表格fetchData的映射关系 generateCollection(schema) @@ -354,6 +370,26 @@ function renderComponent(schema, scope, parent) { return loopList?.length ? loopList.map(renderElement) : renderElement(undefined, 0) } +// 执行用户定义的生命周期函数 +const executeUserLifecycle = (hookName: string, lifeCycleConfig: JSFunction | undefined, context: any) => { + if (!lifeCycleConfig || lifeCycleConfig.type !== 'JSFunction') { + return + } + + try { + const fn = parseData(lifeCycleConfig, {}, context) + if (typeof fn === 'function') { + fn.call(context, context) + } + } catch (error) { + Notify({ + type: 'warning', + title: `${hookName} 生命周期执行失败`, + message: (error as any)?.message || `${hookName} 生命周期函数执行报错,请检查语法` + }) + } +} + export const renderer = defineComponent({ name: 'renderer', props: { @@ -363,6 +399,81 @@ export const renderer = defineComponent({ }, setup(props) { provide('schema', props.schema) + + const context = inject('pageContext') + const lifeCycles = props.parent?.lifeCycles + + // 注入生命周期钩子 + if (lifeCycles?.setup) { + executeUserLifecycle('setup', lifeCycles?.setup, context) + } + + if (lifeCycles?.onBeforeMount) { + onBeforeMount(() => { + executeUserLifecycle('onBeforeMount', lifeCycles.onBeforeMount, context) + }) + } + + if (lifeCycles?.onMounted) { + onMounted(() => { + executeUserLifecycle('onMounted', lifeCycles.onMounted, context) + }) + } + + if (lifeCycles?.onBeforeUpdate) { + onBeforeUpdate(() => { + executeUserLifecycle('onBeforeUpdate', lifeCycles.onBeforeUpdate, context) + }) + } + + if (lifeCycles?.onUpdated) { + onUpdated(() => { + executeUserLifecycle('onUpdated', lifeCycles.onUpdated, context) + }) + } + + if (lifeCycles?.onBeforeUnmount) { + onBeforeUnmount(() => { + executeUserLifecycle('onBeforeUnmount', lifeCycles.onBeforeUnmount, context) + }) + } + + if (lifeCycles?.onUnmounted) { + onUnmounted(() => { + executeUserLifecycle('onUnmounted', lifeCycles.onUnmounted, context) + }) + } + + if (lifeCycles?.onErrorCaptured) { + onErrorCaptured((error, instance, info) => { + try { + const fn = parseData(lifeCycles.onErrorCaptured, {}, context) + if (typeof fn === 'function') { + const result = fn.call(context, error, instance, info) + return result === false + } + } catch (userError) { + Notify({ + type: 'warning', + title: 'onErrorCaptured 生命周期执行失败', + message: (userError as any)?.message || 'onErrorCaptured 生命周期函数执行报错,请检查语法' + }) + } + return true + }) + } + + if (lifeCycles?.onActivated) { + onActivated(() => { + executeUserLifecycle('onActivated', lifeCycles.onActivated, context) + }) + } + + if (lifeCycles?.onDeactivated) { + onDeactivated(() => { + executeUserLifecycle('onDeactivated', lifeCycles.onDeactivated, context) + }) + } }, render() { const context = inject('pageContext') From 417d5f51938c95f86df46d5c8f77b0325dd4d60f Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Mon, 20 Oct 2025 00:14:41 +0800 Subject: [PATCH 33/81] fix: change CanvasBox component to div Change the CanvasBox component to a div to make the component structure of the generated code as consistent as possible with that during low-code rendering, thereby ensuring the consistent behavior of the page's onUpdated and other hook functions. --- .../src/renderer/builtin/CanvasBox.vue | 16 ---------------- .../src/renderer/builtin/index.ts | 2 -- packages/runtime-renderer/src/renderer/render.ts | 7 ++++--- 3 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue deleted file mode 100644 index cfafcce048..0000000000 --- a/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/packages/runtime-renderer/src/renderer/builtin/index.ts b/packages/runtime-renderer/src/renderer/builtin/index.ts index e2a97e3de5..aa303aae9e 100644 --- a/packages/runtime-renderer/src/renderer/builtin/index.ts +++ b/packages/runtime-renderer/src/renderer/builtin/index.ts @@ -11,7 +11,6 @@ */ import CanvasText from './CanvasText.vue' -import CanvasBox from './CanvasBox.vue' import CanvasIcon from './CanvasIcon.vue' import CanvasSlot from './CanvasSlot.vue' import CanvasImg from './CanvasImg.vue' @@ -22,7 +21,6 @@ import CanvasCollection from './CanvasCollection.vue' export { CanvasText, - CanvasBox, CanvasIcon, CanvasSlot, CanvasImg, diff --git a/packages/runtime-renderer/src/renderer/render.ts b/packages/runtime-renderer/src/renderer/render.ts index 96c150a7f4..e2045ff7fa 100644 --- a/packages/runtime-renderer/src/renderer/render.ts +++ b/packages/runtime-renderer/src/renderer/render.ts @@ -37,7 +37,6 @@ import { CanvasSection } from '@opentiny/tiny-engine-builtin-component' import { - CanvasBox, CanvasIcon, CanvasText, CanvasSlot, @@ -55,10 +54,8 @@ const customElements = {} const Mapper = { Icon: CanvasIcon, Text: CanvasText, - div: CanvasBox, Slot: CanvasSlot, slot: CanvasSlot, - Template: CanvasBox, Img: CanvasImg, CanvasRow, CanvasCol, @@ -88,6 +85,10 @@ export const getComponent = (name) => { return component } + if (name === 'Template') { + return 'div' + } + // 如果是 HTML 标签,直接返回 if (isHTMLTag(name)) { return name From e1827cef6e82e601c1e8cef1f8c0af8844bad0d0 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Mon, 20 Oct 2025 08:33:52 +0800 Subject: [PATCH 34/81] fix: update package dependencies --- packages/runtime-renderer/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json index 71d9dd817c..40bedce6a2 100644 --- a/packages/runtime-renderer/package.json +++ b/packages/runtime-renderer/package.json @@ -25,8 +25,8 @@ "vue-router": "^4.2.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^4.0.0", - "typescript": "^5.9.2", + "@vitejs/plugin-vue": "^5.1.2", + "typescript": "^5.4.2", "vite": "^5.4.2" }, "peerDependencies": { From eba84d524760af8039dcb7f62ca905f300a1a2f7 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Mon, 20 Oct 2025 10:40:57 +0800 Subject: [PATCH 35/81] fix: remove debug code from router initialization --- packages/runtime-renderer/src/router/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/router/index.ts index 2a3ab7f74f..9aa7b39cb8 100644 --- a/packages/runtime-renderer/src/router/index.ts +++ b/packages/runtime-renderer/src/router/index.ts @@ -30,8 +30,7 @@ async function createRouterConfig() { depth: page.meta.depth, // 疑问:在嵌套路由中此属性没有改变,此属性和面包屑有关吗? isDefault: page.meta.isDefault, // 用于嵌套路由的默认子路由 hasDefault: false, - defaultPath: '', // 默认子路由的路径 - parentPath: '/' + defaultPath: '' // 默认子路由的路径 } } @@ -87,13 +86,5 @@ export async function createAppRouter() { const routes = await createRouterConfig() const router = createRouter({ history: createWebHashHistory('/runtime.html'), routes }) - if (typeof window !== 'undefined') { - window.__DEBUG_ROUTER__ = router - // eslint-disable-next-line no-console - console.log( - '所有路由:', - router.getRoutes().map((r) => ({ path: r.path, name: r.name, redirect: r.redirect })) - ) - } return router } From 6128c9ae20fa6f69e087e4a1e1e88f0447ab525a Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 21 Oct 2025 00:04:44 +0800 Subject: [PATCH 36/81] Refactor data source initialization and exports Refactored data source logic to use an explicit initDataSource function and getDataSource accessor, improving modularity and initialization flow. Updated related imports and exports for consistency, and ensured data sources are initialized within useAppSchema. Minor export adjustments in page-function and app-function modules. --- .../src/app-function/dataSource.ts | 96 +++++++++---------- .../src/app-function/index.ts | 3 + .../src/composables/useAppSchema.ts | 5 +- .../src/renderer/RenderMain.ts | 10 +- .../src/renderer/page-function/index.ts | 1 + 5 files changed, 55 insertions(+), 60 deletions(-) create mode 100644 packages/runtime-renderer/src/app-function/index.ts diff --git a/packages/runtime-renderer/src/app-function/dataSource.ts b/packages/runtime-renderer/src/app-function/dataSource.ts index 247bcc1bf9..f9c61e9ae0 100644 --- a/packages/runtime-renderer/src/app-function/dataSource.ts +++ b/packages/runtime-renderer/src/app-function/dataSource.ts @@ -1,12 +1,5 @@ import useHttp from './http' -import { useAppSchema } from '../composables/useAppSchema' import { parseJSFunction } from '../utils/data-utils' - -const { dataSourceConfig } = useAppSchema() - -// 深拷贝防止修改原始 reactive -const rawConfig = JSON.parse(JSON.stringify(dataSourceConfig.value || {})) - // 将原本的配置格式标准化以方便复用出码逻辑 const normalizeItem = (item: any) => { return { @@ -23,14 +16,9 @@ const normalizeItem = (item: any) => { } } -const dataSources = { - dataHandler: rawConfig.dataHandler, - list: (rawConfig.list || []).map(normalizeItem) -} - -export const dataSourceMap: Record = {} +const dataSourceMap: Record = {} -const globalDataHandle = dataSources.dataHandler ? parseJSFunction(dataSources.dataHandler) : (res) => res +let globalDataHandle: (res: any) => any = (res) => res // 统一的 load 构造 const load = (http, options, dataSource, shouldFetch) => (params?, customUrl?) => { @@ -70,52 +58,54 @@ const load = (http, options, dataSource, shouldFetch) => (params?, customUrl?) = return http.request(config) } -// 构建每个数据源 -dataSources.list.forEach((config) => { - const http = useHttp(globalDataHandle) - const dataSource = { - config: config, - status: 'init', - data: { data: config.data } // 保持占位,后续 remote 成功后再写 +export const initDataSource = (config: any) => { + Object.keys(dataSourceMap).forEach((key) => delete dataSourceMap[key]) + + if (!config) { + return dataSourceMap } - dataSourceMap[config.name] = dataSource + const normalized = (config.list || []).map(normalizeItem) - const shouldFetch = config.shouldFetch?.value ? parseJSFunction(config.shouldFetch) : () => true - const willFetch = config.willFetch?.value ? parseJSFunction(config.willFetch) : (options) => options + globalDataHandle = config.dataHandler ? parseJSFunction(config.dataHandler) : (res) => res - const dataHandler = (res) => { - const handled = config.dataHandler?.value ? parseJSFunction(config.dataHandler)(res) : res - dataSource.status = 'loaded' - dataSource.data = handled - return handled - } + normalized.forEach((item) => { + const http = useHttp(globalDataHandle) + const dataSource = { + config: item, + status: 'init', + data: { data: item.data }, + load: null // 将在下面设置 + } - const errorHandler = (error) => { - if (config.errorHandler?.value) { - parseJSFunction(config.errorHandler)(error) + const shouldFetch = item.shouldFetch?.value ? parseJSFunction(item.shouldFetch) : () => true + const willFetch = item.willFetch?.value ? parseJSFunction(item.willFetch) : (options) => options + const dataHandler = (res) => { + const handled = item.dataHandler?.value ? parseJSFunction(item.dataHandler)(res) : res + dataSource.status = 'loaded' + dataSource.data = handled + return handled + } + const errorHandler = (error) => { + if (item.errorHandler?.value) { + parseJSFunction(item.errorHandler)(error) + } + dataSource.status = 'error' + dataSource.error = error + return Promise.reject(error) } - dataSource.status = 'error' - dataSource.error = error - return Promise.reject(error) - } - http.interceptors.request.use(willFetch, errorHandler) - http.interceptors.response.use(dataHandler, errorHandler) - - if (import.meta.env.VITE_APP_MOCK === 'mock') { - http.mock([ - { - url: config.options?.uri, - response() { - return Promise.resolve([200, { data: config.data }]) - } - }, - { url: '*', proxy: '*' } - ]) - } + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + + // 设置 load 方法 + dataSource.load = load(http, item.options, dataSource, shouldFetch) + + // 存储到映射中 + dataSourceMap[item.name] = dataSource + }) +} - dataSource.load = load(http, config.options, dataSource, shouldFetch) -}) +export const getDataSource = () => dataSourceMap export default dataSourceMap diff --git a/packages/runtime-renderer/src/app-function/index.ts b/packages/runtime-renderer/src/app-function/index.ts new file mode 100644 index 0000000000..457913d0df --- /dev/null +++ b/packages/runtime-renderer/src/app-function/index.ts @@ -0,0 +1,3 @@ +export * from './dataSource' +export * from './nativeComponents' +export * from './utils' diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index 25d41cd8db..fff86a5a27 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -2,7 +2,7 @@ import { ref, computed, readonly } from 'vue' import type { AppSchema, Util, BlockItem, BlockContent, I18nConfig, ComponentMap, PackageConfig } from '../types/schema' import { initUtils } from '../app-function/utils' import i18n from '@opentiny/tiny-engine-i18n-host' -import { addStyle, getComponents, loadPackageDependencys } from '../app-function/nativeComponents' +import { addStyle, getComponents, loadPackageDependencys, initDataSource } from '../app-function' const appSchema = ref(null) const isLoading = ref(false) @@ -76,6 +76,9 @@ export function useAppSchema() { // 初始化工具函数 await initializeUtils(schema.data.utils) + // 初始化数据源 + initDataSource(schema.data.dataSource) + // 注入全局CSS injectGlobalCSS(schema.data.css) } diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index ef95a609f0..914681ce4e 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -12,16 +12,14 @@ import { h, computed, provide, nextTick, reactive, watch, defineComponent, inject } from 'vue' import Loading from '../components/Loading.vue' -import { parseData } from './parser/parser.ts' +import { parseData } from './parser' import { renderer } from './render.ts' -import { setPageCss } from './page-function/css-handler.ts' -import { useState } from './page-function/state' +import { setPageCss, useState } from './page-function' import useContext from './useContext.ts' import { useRouter, useRoute } from 'vue-router' import { useAppSchema } from '../composables/useAppSchema' import type { PageContent as Schema } from '../types/schema' -import dataSourceMap from '../app-function/dataSource.ts' -import { getUtilsAll } from '../app-function/utils.ts' +import { getDataSource, getUtilsAll } from '../app-function' interface Props { pageId: number @@ -84,7 +82,7 @@ export default defineComponent({ route, router, stores, - dataSourceMap, + dataSourceMap: getDataSource(), utils: getUtilsAll() } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 diff --git a/packages/runtime-renderer/src/renderer/page-function/index.ts b/packages/runtime-renderer/src/renderer/page-function/index.ts index a574641651..b2cef2dfea 100644 --- a/packages/runtime-renderer/src/renderer/page-function/index.ts +++ b/packages/runtime-renderer/src/renderer/page-function/index.ts @@ -11,3 +11,4 @@ */ export * from './css-handler' +export * from './state' From 09924218491cf0c225640920ec26d308ebee6b13 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Tue, 21 Oct 2025 00:11:10 +0800 Subject: [PATCH 37/81] feat: remove focus logic for existing runtime window Considering that users usually click the runtime-deploy button again in a window with the runtime Renderer enabled only after making some modifications in the designer, the logic of calling runtimeWindow.focus() when the window is already open has been changed to fetching a new AppSchema and reloading it every time the runtime-deploy button is clicked. --- packages/common/js/runtime-deploy.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/common/js/runtime-deploy.js b/packages/common/js/runtime-deploy.js index b043f7aa74..9bb66089c0 100644 --- a/packages/common/js/runtime-deploy.js +++ b/packages/common/js/runtime-deploy.js @@ -48,16 +48,6 @@ export const deployPage = () => { export const runtimeDeploy = async () => { const { openUrl } = await deployPage() - if (runtimeWindow && !runtimeWindow.closed) { - try { - runtimeWindow.focus() - } catch (e) { - // eslint-disable-next-line no-console - console.warn('[runtime-deploy] focus runtime window failed:', e) - } - return - } - runtimeWindow = window.open(openUrl, 'tiny-engine-runtime') if (!runtimeWindow) { useNotify({ From 1fab0a6867137189f8f4e243ca8419c12c1d9f70 Mon Sep 17 00:00:00 2001 From: littla2liyou <1446638814@qq.com> Date: Wed, 22 Oct 2025 18:43:06 +0800 Subject: [PATCH 38/81] fix: refactor CSS scoping with PostCSS and selector parser --- packages/runtime-renderer/package.json | 4 +- .../src/components/PageRenderer.ts | 4 +- .../src/renderer/RenderMain.ts | 17 +- .../renderer/page-function/blockContext.ts | 28 ++- .../src/renderer/page-function/css-handler.ts | 110 ---------- .../src/renderer/page-function/css.ts | 152 ++++++++++++++ .../src/renderer/page-function/index.ts | 2 +- .../page-function/scope-css-plugin.ts | 193 ++++++++++++++++++ .../runtime-renderer/src/renderer/render.ts | 24 ++- 9 files changed, 404 insertions(+), 130 deletions(-) delete mode 100644 packages/runtime-renderer/src/renderer/page-function/css-handler.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/css.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/scope-css-plugin.ts diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json index 40bedce6a2..422a7d1da1 100644 --- a/packages/runtime-renderer/package.json +++ b/packages/runtime-renderer/package.json @@ -22,7 +22,9 @@ "axios": "^1.10.0", "axios-mock-adapter": "^1.19.0", "pinia": "^2.1.0", - "vue-router": "^4.2.0" + "vue-router": "^4.2.0", + "postcss": "^8.4.31", + "postcss-selector-parser": "^7.0.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.2", diff --git a/packages/runtime-renderer/src/components/PageRenderer.ts b/packages/runtime-renderer/src/components/PageRenderer.ts index 688260d40d..c53967dfce 100644 --- a/packages/runtime-renderer/src/components/PageRenderer.ts +++ b/packages/runtime-renderer/src/components/PageRenderer.ts @@ -11,11 +11,11 @@ export function withPageRenderer(WrappedComponent: any) { } }, setup(props) { + const key = `data-te-page-${props.pageId}` return () => { return h(WrappedComponent, { pageId: props.pageId, - // ref: pageInstance, - key: props.pageId + cssScopeId: key }) } } diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 914681ce4e..ba82f049dd 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -23,6 +23,7 @@ import { getDataSource, getUtilsAll } from '../app-function' interface Props { pageId: number + cssScopeId: string } export default defineComponent({ @@ -31,10 +32,15 @@ export default defineComponent({ pageId: { type: Number, default: 0 + }, + cssScopeId: { + type: String, + default: null } }, setup(props: Props) { const { getPageById } = useAppSchema() + //clearAllPageCSS() const currentSchema = computed(() => { const page = getPageById(props.pageId) // 通过 pageId 获取最新的页面对象 @@ -77,16 +83,19 @@ export default defineComponent({ const newSchema = JSON.parse(JSON.stringify(data)) - const context = { + const cssScopeId = props.cssScopeId || `data-te-page-${String(props.pageId) || 'render-main'}` + const contextData = { state, route, router, stores, dataSourceMap: getDataSource(), - utils: getUtilsAll() + utils: getUtilsAll(), + cssScopeId, + getCssScopeId: () => cssScopeId } // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 - setContext(context, true) + setContext(contextData, true) // 设置方法调用上下文 setMethods(newSchema.methods, true) @@ -94,7 +103,7 @@ export default defineComponent({ // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods setState(newSchema.state, true) await nextTick() - setPageCss(data.css || '', String(props.pageId) || 'render-main') + setPageCss(data.css || '', cssScopeId) Object.assign(pageSchema, newSchema) } diff --git a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts index 780283cb6a..d17524884a 100644 --- a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts +++ b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts @@ -1,3 +1,4 @@ +// @ts-nocheck /** * Copyright (c) 2023 - present TinyEngine Authors. * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. @@ -11,7 +12,7 @@ */ import { nextTick, inject } from 'vue' -import { getCSSHandler } from './css-handler.ts' +import { getCSSHandler } from './css.ts' import { parseData } from '../parser/parser.ts' import { useState } from './state.ts' import useContext from '../useContext.ts' @@ -19,9 +20,21 @@ import type { PageContent as Schema } from '../../types/schema.ts' import dataSourceMap from '../../app-function/dataSource.js' import { getUtilsAll } from '../../app-function/utils.ts' +const invalidateCharRE = /[^a-z0-9-]/g + +export const getBlockCssScopeId = (fileName?: string) => { + const normalized = String(fileName || 'unknown') + .toLowerCase() + .replace(invalidateCharRE, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + return `data-te-page-block-${normalized || 'unknown'}` +} + // 创建 context 实例的工厂函数 export const createBlockContext = () => { - const { context, setContext, getContext } = useContext() + const contextApi = useContext() as any + const { context, setContext, getContext } = contextApi const stores = inject('stores') const methods: Record = {} const { state, setState } = useState({ getContext }) @@ -46,11 +59,14 @@ export const createBlockContext = () => { const newSchema = JSON.parse(JSON.stringify(data)) + const cssScopeId = getBlockCssScopeId(data.fileName) const contextData = { state, stores, dataSourceMap, - utils: getUtilsAll() + utils: getUtilsAll(), + cssScopeId, + getCssScopeId: () => cssScopeId } setContext(contextData, true) setMethods(newSchema.methods, true) @@ -58,8 +74,8 @@ export const createBlockContext = () => { await nextTick() const cssHandler = getCSSHandler({ enableScoped: true }) - cssHandler.setPageCss(data.css || '', `block-${data.fileName || 'unknown'}`) - + cssHandler.setPageCss(data.css || '', cssScopeId) + //console.log('setPageCss', data.css) return context } @@ -70,7 +86,7 @@ export const createBlockContext = () => { } // 暂时不写成异步函数形式,方便后续调用 -export const getBlockContext = (schema: Schema) => { +export const getBlockContext = (schema: Schema): Record => { const blockContext = createBlockContext() blockContext.setSchema(schema) return blockContext.getContext() diff --git a/packages/runtime-renderer/src/renderer/page-function/css-handler.ts b/packages/runtime-renderer/src/renderer/page-function/css-handler.ts deleted file mode 100644 index 81bfc10d79..0000000000 --- a/packages/runtime-renderer/src/renderer/page-function/css-handler.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -interface CSSHandlerOptions { - pageId?: string - enableScoped?: boolean - enableModernCSS?: boolean -} - -const fallbackStyleMap = new Map() -let enableScoped = true - -// 简化版的作用域处理,将CSS选择器添加页面作用域 -function processScopedCSS(key: string, css: string): string { - if (!enableScoped) { - return css - } - - // 简单的CSS作用域处理 - // 将 body, html 等全局选择器转换为作用域选择器 - return css - .replace(/body\s*{/g, `body[${key}] {`) - .replace(/html\s*{/g, `html[${key}] {`) - .replace(/\*:global\(([^)]+)\)/g, '$1') // 处理:global()语法 - .replace(/:global\(([^)]+)\)/g, '$1') // 处理:global()语法 -} - -// 使用传统方式设置样式 -function setCSS(key: string, css: string): void { - let styleElement = fallbackStyleMap.get(key) - - if (!styleElement) { - styleElement = document.createElement('style') - styleElement.setAttribute('type', 'text/css') - styleElement.setAttribute('data-te-page', key) - document.head?.appendChild(styleElement) - fallbackStyleMap.set(key, styleElement) - } - - // 处理作用域CSS - const processedCSS = enableScoped ? processScopedCSS(key, css) : css - - styleElement.textContent = processedCSS -} - -// 移除页面CSS -function removePageCss(key: string): void { - const styleElement = fallbackStyleMap.get(key) - if (styleElement && styleElement.parentNode) { - styleElement.parentNode.removeChild(styleElement) - fallbackStyleMap.delete(key) - } -} - -// 清理所有样式 -function clearAllStyles(): void { - fallbackStyleMap.forEach((_, key) => { - removePageCss(key) - }) -} - -// 设置页面CSS (保持原有API) -export function setPageCss(css: string = '', pageId?: string): void { - const cssPageId = pageId || 'default' - const key = `data-te-page-${cssPageId}` - - if (!css) { - removePageCss(key) - return - } - - setCSS(key, css) -} - -// 清理所有CSS(用于页面切换)(保持原有API) -export function clearAllPageCSS(): void { - clearAllStyles() -} - -// 获取全局CSS处理器 (保持向后兼容) -export function getCSSHandler(options?: CSSHandlerOptions): { - setPageCss: (css?: string, pageId?: string) => void - clearAllStyles: () => void - removePageCss: (key: string) => void -} { - if (options?.enableScoped !== undefined) { - enableScoped = options.enableScoped - } - - return { - setPageCss, - clearAllStyles, - removePageCss - } -} - -// 保持与之前相同的导出接口 -export default { - setPageCss, - clearAllPageCSS -} diff --git a/packages/runtime-renderer/src/renderer/page-function/css.ts b/packages/runtime-renderer/src/renderer/page-function/css.ts new file mode 100644 index 0000000000..51ff203452 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/css.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import postcss from 'postcss' +import scopedPlugin from './scope-css-plugin' + +interface CSSHandlerOptions { + pageId?: string + enableScoped?: boolean + enableModernCSS?: boolean +} + +type AdoptedSheets = CSSStyleSheet[] | undefined + +const supportsAdoptedStyleSheet = + typeof document !== 'undefined' && Array.isArray((document as any)?.adoptedStyleSheets) +const styleSheetMap = new Map() +const fallbackStyleMap = new Map() +let enableScoped = true + +function normalizeScopeKey(pageId?: string): string { + if (!pageId) { + return 'data-te-page-default' + } + return pageId.startsWith('data-te-page-') ? pageId : `data-te-page-${pageId}` +} + +function ensureAdoptedStyleSheet(key: string): CSSStyleSheet { + let sheet = styleSheetMap.get(key) + if (!sheet) { + sheet = new CSSStyleSheet() + styleSheetMap.set(key, sheet) + const adoptedSheets = ((document as any).adoptedStyleSheets || []) as AdoptedSheets + if (!adoptedSheets?.includes(sheet)) { + ;(document as any).adoptedStyleSheets = [...(adoptedSheets || []), sheet] + } + } + return sheet +} + +function ensureFallbackStyleElement(key: string): HTMLStyleElement { + let styleElement = fallbackStyleMap.get(key) + if (!styleElement) { + styleElement = document.createElement('style') + styleElement.type = 'text/css' + styleElement.setAttribute('data-te-page', key) + document.head?.appendChild(styleElement) + fallbackStyleMap.set(key, styleElement) + } + return styleElement +} + +function processScopedCss(key: string, css: string): Promise { + if (!enableScoped) { + return Promise.resolve(css) + } + + return postcss([scopedPlugin(key)]) + .process(css, { from: undefined }) + .then((result) => result.css) +} + +function applyCss(key: string, css: string): void { + if (supportsAdoptedStyleSheet && typeof CSSStyleSheet !== 'undefined') { + const sheet = ensureAdoptedStyleSheet(key) + processScopedCss(key, css) + .then((scopedCss) => { + sheet.replaceSync(scopedCss) + }) + .catch(() => { + sheet.replaceSync(css) + }) + return + } + + const styleElement = ensureFallbackStyleElement(key) + processScopedCss(key, css) + .then((scopedCss) => { + styleElement.textContent = scopedCss + }) + .catch(() => { + styleElement.textContent = css + }) +} + +function removePageCss(key: string): void { + const sheet = styleSheetMap.get(key) + if (sheet) { + styleSheetMap.delete(key) + const adoptedSheets = ((document as any).adoptedStyleSheets || []) as AdoptedSheets + if (adoptedSheets?.length) { + ;(document as any).adoptedStyleSheets = adoptedSheets.filter((item) => item !== sheet) + } + } + + const styleElement = fallbackStyleMap.get(key) + if (styleElement?.parentNode) { + styleElement.parentNode.removeChild(styleElement) + fallbackStyleMap.delete(key) + } +} + +function clearAllStyles(): void { + styleSheetMap.forEach((_, key) => removePageCss(key)) + fallbackStyleMap.forEach((_, key) => removePageCss(key)) +} + +export function setPageCss(css: string = '', pageId?: string): void { + const key = normalizeScopeKey(pageId) + + if (!css) { + removePageCss(key) + return + } + //console.log('setPageCss key', key, css) + + applyCss(key, css) +} + +export function clearAllPageCSS(): void { + clearAllStyles() +} + +export function getCSSHandler(options?: CSSHandlerOptions): { + setPageCss: (css?: string, pageId?: string) => void + clearAllStyles: () => void + removePageCss: (key: string) => void +} { + if (options?.enableScoped !== undefined) { + enableScoped = options.enableScoped + } + + return { + setPageCss, + clearAllStyles, + removePageCss: (key: string) => removePageCss(normalizeScopeKey(key)) + } +} + +export default { + setPageCss, + clearAllPageCSS +} diff --git a/packages/runtime-renderer/src/renderer/page-function/index.ts b/packages/runtime-renderer/src/renderer/page-function/index.ts index b2cef2dfea..07d4adb658 100644 --- a/packages/runtime-renderer/src/renderer/page-function/index.ts +++ b/packages/runtime-renderer/src/renderer/page-function/index.ts @@ -10,5 +10,5 @@ * */ -export * from './css-handler' +export * from './css' export * from './state' diff --git a/packages/runtime-renderer/src/renderer/page-function/scope-css-plugin.ts b/packages/runtime-renderer/src/renderer/page-function/scope-css-plugin.ts new file mode 100644 index 0000000000..297a99c50c --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/scope-css-plugin.ts @@ -0,0 +1,193 @@ +/** @ref {@vue/compiler-sfc@2.7.16/src/stylePlugins/scoped.ts } */ +/* eslint-disable @typescript-eslint/no-use-before-define, prefer-const*/ +import { type PluginCreator, Rule, AtRule } from 'postcss' +import selectorParser from 'postcss-selector-parser' + +const animationNameRE = /^(-\w+-)?animation-name$/ +const animationRE = /^(-\w+-)?animation$/ + +const scopedPlugin: PluginCreator = (id = '') => { + const keyframes = Object.create(null) + const shortId = id.replace(/^data-v-/, '') + + return { + postcssPlugin: 'vue-sfc-scoped', + Rule(rule) { + processRule(id, rule) + }, + AtRule(node) { + if (/-?keyframes$/.test(node.name) && !node.params.endsWith(`-${shortId}`)) { + // register keyframes + keyframes[node.params] = node.params = node.params + '-' + shortId + } + }, + OnceExit(root) { + if (Object.keys(keyframes).length) { + // If keyframes are found in this