From 8055d3c4a42350d1db7200aceec2f3ab85bd8322 Mon Sep 17 00:00:00 2001 From: yy-wow Date: Thu, 24 Oct 2024 02:18:37 -0700 Subject: [PATCH 01/26] refactor: split renderer package --- jsconfig.json | 1 + .../get/app-center/v1/apps/schema/918.json | 8 - .../src/vite-plugins/devAliasPlugin.js | 1 + packages/renderer/.eslintrc.cjs | 42 + packages/renderer/.gitignore | 24 + packages/renderer/README.md | 36 + packages/renderer/package.json | 61 ++ .../render => renderer}/src/CanvasEmpty.vue | 0 packages/renderer/src/RenderMain.js | 470 +++++++++++ packages/renderer/src/Renderer.js | 3 + packages/renderer/src/builtin/CanvasBox.vue | 22 + .../renderer/src/builtin/CanvasCollection.js | 278 +++++++ .../renderer/src/builtin/CanvasCollection.vue | 106 +++ packages/renderer/src/builtin/CanvasIcon.vue | 30 + packages/renderer/src/builtin/CanvasImg.vue | 18 + .../src/builtin/CanvasPlaceholder.vue | 26 + packages/renderer/src/builtin/CanvasSlot.vue | 22 + packages/renderer/src/builtin/CanvasText.vue | 18 + packages/renderer/src/builtin/builtin.json | 540 +++++++++++++ packages/renderer/src/builtin/helper.js | 46 ++ packages/renderer/src/builtin/index.js | 21 + packages/renderer/src/constants.js | 3 + packages/renderer/src/context.js | 57 ++ packages/renderer/src/index.js | 16 + packages/renderer/src/render.js | 733 ++++++++++++++++++ packages/renderer/vite.config.js | 51 ++ 26 files changed, 2625 insertions(+), 8 deletions(-) create mode 100644 packages/renderer/.eslintrc.cjs create mode 100644 packages/renderer/.gitignore create mode 100644 packages/renderer/README.md create mode 100644 packages/renderer/package.json rename packages/{canvas/render => renderer}/src/CanvasEmpty.vue (100%) create mode 100644 packages/renderer/src/RenderMain.js create mode 100644 packages/renderer/src/Renderer.js create mode 100644 packages/renderer/src/builtin/CanvasBox.vue create mode 100644 packages/renderer/src/builtin/CanvasCollection.js create mode 100644 packages/renderer/src/builtin/CanvasCollection.vue create mode 100644 packages/renderer/src/builtin/CanvasIcon.vue create mode 100644 packages/renderer/src/builtin/CanvasImg.vue create mode 100644 packages/renderer/src/builtin/CanvasPlaceholder.vue create mode 100644 packages/renderer/src/builtin/CanvasSlot.vue create mode 100644 packages/renderer/src/builtin/CanvasText.vue create mode 100644 packages/renderer/src/builtin/builtin.json create mode 100644 packages/renderer/src/builtin/helper.js create mode 100644 packages/renderer/src/builtin/index.js create mode 100644 packages/renderer/src/constants.js create mode 100644 packages/renderer/src/context.js create mode 100644 packages/renderer/src/index.js create mode 100644 packages/renderer/src/render.js create mode 100644 packages/renderer/vite.config.js diff --git a/jsconfig.json b/jsconfig.json index 06993f594..2843d9607 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -7,6 +7,7 @@ "@opentiny/tiny-engine": ["packages/design-core/index.js"], "@opentiny/tiny-engine-meta-register": ["packages/register/src/index.js"], "@opentiny/tiny-engine-canvas": ["packages/canvas/src/index.js"], + "@opentiny/tiny-engine-renderer": ["packages/renderer/src/index.js"], "@opentiny/tiny-engine-plugin-materials": ["packages/plugins/materials/index"], "@opentiny/tiny-engine-plugin-state": ["packages/plugins/state/index"], "@opentiny/tiny-engine-plugin-script": ["packages/plugins/script/index"], diff --git a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json index 17656258e..6a49846a8 100644 --- a/mockServer/src/mock/get/app-center/v1/apps/schema/918.json +++ b/mockServer/src/mock/get/app-center/v1/apps/schema/918.json @@ -2130,14 +2130,6 @@ "main": "" } }, - { - "name": "npm", - "type": "function", - "content": { - "type": "JSFunction", - "value": "''" - } - }, { "name": "Pager", "type": "npm", diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js index 87ed3c29a..9c6171cf3 100644 --- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js +++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js @@ -56,6 +56,7 @@ const getDevAlias = (useSourceAlias) => { '@opentiny/tiny-engine-http': path.resolve(basePath, 'packages/http/src/index.js'), '@opentiny/tiny-engine-canvas': path.resolve(basePath, 'packages/canvas/index.js'), '@opentiny/tiny-engine-canvas/render': path.resolve(basePath, 'packages/canvas/render/index.js'), + '@opentiny/tiny-engine-renderer': path.resolve(basePath, 'packages/renderer/src/index.js'), '@opentiny/tiny-engine-utils': path.resolve(basePath, 'packages/utils/src/index.js'), '@opentiny/tiny-engine-webcomponent-core': path.resolve(basePath, 'packages/webcomponent/src/lib.js'), '@opentiny/tiny-engine-i18n-host': path.resolve(basePath, 'packages/i18n/src/lib.js'), diff --git a/packages/renderer/.eslintrc.cjs b/packages/renderer/.eslintrc.cjs new file mode 100644 index 000000000..7e042cff2 --- /dev/null +++ b/packages/renderer/.eslintrc.cjs @@ -0,0 +1,42 @@ +/** +* 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. +* +*/ + +module.exports = { + env: { + browser: true, + es2015: true, + node: true, + jest: true + }, + extends: ['eslint:recommended', 'plugin:vue/vue3-essential'], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@babel/eslint-parser', + ecmaVersion: 'latest', + sourceType: 'module', + requireConfigFile: false, + babelOptions: { + parserOpts: { + plugins: ['jsx'] + } + } + }, + plugins: ['vue'], + rules: { + 'no-console': 'error', + 'no-debugger': 'error', + 'space-before-function-paren': 'off', + 'vue/multi-word-component-names': 'off', + 'no-use-before-define': 'error', + 'no-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }] + } +} diff --git a/packages/renderer/.gitignore b/packages/renderer/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/packages/renderer/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/renderer/README.md b/packages/renderer/README.md new file mode 100644 index 000000000..63f827de3 --- /dev/null +++ b/packages/renderer/README.md @@ -0,0 +1,36 @@ +# tiny-engine-canvas + +## build + +Note: tiny-engine-canvas module contains two parts, *canvas-container* and *canvas*, and canvas is rendered in iframe window +During the build phase, they are built separately. The build products use different external strategies, and the *canvas* product will be embedded in base64 format within the *canvas-container* product, +since the library-mode-build now only supports embed dependent assets in base64 format, and the embedded base64 format is more general for other build and pack tools. +You should notice the difference between the products of *canvas-container* and *canvas*. The product of *canvas-container* works fine with other *tiny-engine-\** packages with unfixed versions. The product of *canvas* only externalizes `vue` and `vue-i18n`; the rest of the dependent packages will be packed and won't be replaceable with other versions. (This means you will not be able to joint-debug the dependent packages inside the *canvas* product code. You should joint-debug using the canvas module source code.) + +Develop and debug the canvas module in development mode requires: + +1) Setting the Vite config with `devAlias` (`resolve.alias`), which points the canvas package name to the canvas module source code. +2) Using the `canvas-dev-external` plugin. + +Vite uses esbuild in development. The `devAlias` configuration makes the source code of the canvas module work and allows for joint debugging. +However, esbuild won't perceive the external configuration for rollup; it will resolve `vue` and other dependencies (that we originally wanted to exclude and let them naturally point to the package addresses in the `importmap`) to `node_modules`. +For this reason, we need the `canvas-dev-external` plugin. It can externalize the dependencies specified in the `importmap` of *canvas* for esbuild. +On the other hand, externalizing the dependencies will affect all other packages in the same runtime. This plugin will generate another `importmap` that points all affected dependencies to `node_modules` to eliminate the side effects. + +Finally, modules inside and outside the *canvas* iframe can joint-debug well, and we accomplish the goal of decoupling the versions of `vue` and other dependencies inside and outside the *canvas*. (That is, we can use completely different versions of `vue` inside or outside the *canvas*; we don’t need to synchronize the versions. +In some cases, we may want to use a lower or higher version for better support.) + +## 构建 + +注意: tiny-engine-canvas模块目前含有两部分,*画布容器*和*画布*, 其中*画布*在iframe内进行渲染。 +在构建阶段canvas包使用分开构建,在构建完的产物中,*画布容器*和*画布*使用了不同的external策略,打包完后*画布*源码会base64内嵌到*画布容器*源码中。 +目前库打包依赖资源仅支持base64,且base64内嵌普适性较高,在其他非vite打包工具下能正常工作。 +所以使用canvas包产物应该注意到,*画布容器*打包后仍然可以和其他*tiny-engine-\**的包的不同版本配合工作,而*画布*本身打包完后内容就固化了,不会再和其他包联动。 + +开发态开发和调试canvas包需要: +1) vite配置中 devAlias(`resolve.alias`)将canvas包名指向canvas的src源码 +2) 搭配canvas-dev-external插件使用 +当前开发态vite使用了esbuild, devAlias配置将能正常工作和联调, 但是esbuild不接收来自rollup配置的externals,将导致原本画布应该走`importmap`解析的内容指向了`node_modules`, +故需要搭配canvas-dev-external插件,一方面将*画布*的external项排除,另一方面,对*画布*外也造成的影响将通过另一个指向`node_modules`的`importmap`进行补偿 + +最后,通过以上方式,画布内外能够很好地联调,并且完成了画布内外vue版本的解耦(即可以支持使用完全不同的vue版本) diff --git a/packages/renderer/package.json b/packages/renderer/package.json new file mode 100644 index 000000000..55141672b --- /dev/null +++ b/packages/renderer/package.json @@ -0,0 +1,61 @@ +{ + "name": "@opentiny/tiny-engine-renderer", + "version": "2.0.0-alpha.4", + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "main": "dist/index.js", + "module": "dist/index.js", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/opentiny/tiny-engine", + "directory": "packages/canvas" + }, + "bugs": { + "url": "https://github.com/opentiny/tiny-engine/issues" + }, + "author": "OpenTiny Team", + "license": "MIT", + "homepage": "https://opentiny.design/tiny-engine", + "type": "module", + "dependencies": { + "@babel/core": "7.18.13", + "@opentiny/tiny-engine-builtin-component": "workspace:*", + "@opentiny/tiny-engine-common": "workspace:*", + "@opentiny/tiny-engine-http": "workspace:*", + "@opentiny/tiny-engine-i18n-host": "workspace:*", + "@opentiny/tiny-engine-meta-register": "workspace:*", + "@opentiny/tiny-engine-utils": "workspace:*", + "@opentiny/tiny-engine-webcomponent-core": "workspace:*", + "@vue/babel-plugin-jsx": "1.1.1", + "@vue/shared": "^3.3.4", + "@vueuse/core": "^9.6.0" + }, + "devDependencies": { + "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", + "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue-jsx": "^4.0.1", + "rollup-plugin-polyfill-node": "^0.13.0", + "vite": "^5.4.2" + }, + "peerDependencies": { + "@opentiny/vue": "^3.14.0", + "@opentiny/vue-icon": "^3.14.0", + "@opentiny/vue-renderless": "^3.14.0", + "vue": "^3.4.15", + "vue-i18n": "^9.9.0" + } +} diff --git a/packages/canvas/render/src/CanvasEmpty.vue b/packages/renderer/src/CanvasEmpty.vue similarity index 100% rename from packages/canvas/render/src/CanvasEmpty.vue rename to packages/renderer/src/CanvasEmpty.vue diff --git a/packages/renderer/src/RenderMain.js b/packages/renderer/src/RenderMain.js new file mode 100644 index 000000000..c057e0f8c --- /dev/null +++ b/packages/renderer/src/RenderMain.js @@ -0,0 +1,470 @@ +/** + * 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, nextTick, shallowReactive, reactive, ref, watch, watchEffect } from 'vue' +import { I18nInjectionKey } from 'vue-i18n' +import TinyVue from '@opentiny/vue' +import * as TinyVueIcon from '@opentiny/vue-icon' +import { useBroadcastChannel } from '@vueuse/core' +import { constants, utils as commonUtils } from '@opentiny/tiny-engine-utils' +import renderer, { + parseData, + setConfigure, + setController, + globalNotify, + isStateAccessor, + generateFn, + getCollectionMethodsMap, + getBlockSlotDataMap, + getComponent +} from './render' +import { + getNode as getNodeById, + clearNodes, + getRoot, + setContext, + getContext, + setCondition, + getCondition, + getConditions, + context, + setNode +} from './context' +import CanvasEmpty from './CanvasEmpty.vue' + +const { BROADCAST_CHANNEL } = constants +const { parseFunction: generateFunction } = commonUtils + +const reset = (obj) => { + Object.keys(obj).forEach((key) => delete obj[key]) +} + +const refreshKey = ref(0) +const methods = {} +const schema = reactive({}) +const state = shallowReactive({}) +const bridge = {} +const utils = {} +const props = {} + +const globalState = ref([]) +const stores = shallowReactive({}) +const dataSourceMap = shallowReactive({}) + +watchEffect(() => { + reset(stores) + globalState.value.forEach(({ id, state = {}, getters = {} }) => { + const computedGetters = Object.keys(getters).reduce( + (acc, key) => ({ + ...acc, + [key]: parseData(getters[key], state, acc) + }), + {} + ) + + stores[id] = { ...state, ...computedGetters } + }) +}) + +const getUtils = () => utils + +const setUtils = (data, clear, isForceRefresh) => { + if (clear) { + reset(utils) + } + const utilsCollection = {} + // 目前画布还不具备远程加载utils工具类的功能,目前只能加载TinyVue组件库中的组件工具 + data?.forEach((item) => { + const util = TinyVue[item.content.exportName] + if (util) { + utilsCollection[item.name] = util + } + + // 此处需要把工具类中的icon图标也加入utils上下文环境 + const utilIcon = TinyVueIcon[item.content.exportName] + if (utilIcon) { + utilsCollection[item.name] = utilIcon + } + + // 解析函数式的工具类 + if (item.type === 'function') { + const defaultFn = () => {} + utilsCollection[item.name] = generateFunction(item.content.value, context) || defaultFn + } + }) + Object.assign(utils, utilsCollection) + + // 因为工具类并不具有响应式行为,所以需要通过修改key来强制刷新画布 + if (isForceRefresh) { + refreshKey.value++ + } +} + +const updateUtils = (data) => { + setUtils(data, false, true) +} + +const deleteUtils = (data) => { + data?.forEach((item) => { + if (utils[item.name]) { + delete utils[item.name] + } + }) + setUtils([], false, true) +} + +const setBridge = (data, clear) => { + clear && reset(bridge) + Object.assign(bridge, data) +} + +const getBridge = () => bridge + +const getMethods = () => methods + +const setMethods = (data = {}, clear) => { + clear && reset(methods) + // 这里有些方法在画布还是有执行的必要的,比如说表格的renderer和formatText方法,包括一些自定义渲染函数 + Object.assign( + methods, + Object.fromEntries( + Object.keys(data).map((key) => { + return [key, parseData(data[key], {}, getContext())] + }) + ) + ) + setContext(methods) +} + +const getState = () => state + +const deleteState = (variable) => { + delete state[variable] +} + +const generateAccessor = (type, accessor, property) => { + const accessorFn = generateFunction(accessor[type].value, context) + + return { property, accessorFn, type } +} + +// 这里缓存状态变量对应的访问器,用于watchEffect更新和取消监听 +const stateAccessorMap = new Map() + +// 缓存区块属性的访问器 +const propsAccessorMap = new Map() + +const generateStateAccessors = (type, accessor, key) => { + const stateWatchEffectKey = `${key}${type}` + const { property, accessorFn } = generateAccessor(type, accessor, key) + + // 将之前已有的watchEffect取消监听,这里操作很有必要,不然会造成数据混乱 + stateAccessorMap.get(stateWatchEffectKey)?.() + + // 更新watchEffect监听 + stateAccessorMap.set( + stateWatchEffectKey, + watchEffect(() => { + try { + accessorFn() + } catch (error) { + globalNotify({ + type: 'warning', + title: `状态变量${property}的访问器函数:${accessorFn.name}执行报错`, + message: error?.message || `状态变量${property}的访问器函数:${accessorFn.name}执行报错,请检查语法` + }) + } + }) + ) +} + +const setState = (data, clear) => { + clear && reset(state) + if (!schema.state) { + schema.state = data + } + + Object.assign(state, parseData(data, {}, getContext()) || {}) + + // 在状态变量合并之后,执行访问器中watchEffect,为了可以在访问器函数中可以访问其他state变量 + Object.entries(data || {})?.forEach(([key, stateData]) => { + if (isStateAccessor(stateData)) { + const accessor = stateData.accessor + if (accessor?.getter?.value) { + generateStateAccessors('getter', accessor, key) + } + + if (accessor?.setter?.value) { + generateStateAccessors('setter', accessor, key) + } + } + }) +} + +const getDataSourceMap = () => { + return dataSourceMap.value +} + +const setDataSourceMap = (list) => { + dataSourceMap.value = list.reduce((dMap, config) => { + const dataSource = { config: config.data } + + const result = { + code: '', + msg: 'success', + data: {} + } + result.data = + dataSource.config.type === 'array' + ? { items: dataSource?.config?.data, total: dataSource?.config?.data?.length } + : dataSource?.config?.data + + dataSource.load = () => Promise.resolve(result) + dMap[config.name] = dataSource + + return dMap + }, {}) +} + +const getGlobalState = () => { + return globalState.value +} + +const setGlobalState = (data = []) => { + globalState.value = data +} + +const setProps = (data, clear) => { + clear && reset(props) + Object.assign(props, data) +} + +const getProps = () => props + +const initProps = (properties = []) => { + const props = {} + const accessorFunctions = [] + + properties.forEach(({ content = [] }) => { + content.forEach(({ defaultValue, property, accessor }) => { + // 如果没有设置defaultValue就是undefined这和vue处理方式一样 + props[property] = defaultValue + + // 如果区块属性有访问器accessor,则先解析getter和setter函数 + if (accessor?.getter?.value) { + // 此处不能直接执行watchEffect,需要在上下文环境设置好之后去执行,此处只是收集函数 + accessorFunctions.push(generateAccessor('getter', accessor, property)) + } + + if (accessor?.setter?.value) { + accessorFunctions.push(generateAccessor('setter', accessor, property)) + } + }) + }) + + setProps(props, true) + + return accessorFunctions +} + +const getSchema = () => schema + +const setPagecss = (css = '') => { + const id = 'page-css' + let element = document.getElementById(id) + const head = document.querySelector('head') + + document.body.setAttribute('style', '') + + if (!element) { + element = document.createElement('style') + element.setAttribute('type', 'text/css') + element.setAttribute('id', id) + + element.innerHTML = css + head.appendChild(element) + } else { + element.innerHTML = css + } +} + +const setSchema = async (data) => { + const newSchema = JSON.parse(JSON.stringify(data || schema)) + reset(schema) + // 页面初始化的时候取消所有状态变量的watchEffect监听 + stateAccessorMap.forEach((stateAccessorFn) => { + stateAccessorFn() + }) + + // 区块初始化的时候取消所有的区块属性watchEffect监听 + propsAccessorMap.forEach((propsAccessorFn) => { + propsAccessorFn() + }) + + // 清空存状态变量和区块props访问器的缓存 + stateAccessorMap.clear() + propsAccessorMap.clear() + + const context = { + utils, + bridge, + stores, + state, + props, + dataSourceMap: {}, + emit: () => {} // 兼容访问器中getter和setter中this.emit写法 + } + Object.defineProperty(context, 'dataSourceMap', { + get: getDataSourceMap + }) + // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 + setContext(context, true) + + // 设置方法调用上下文 + setMethods(newSchema.methods, true) + + // 如果是区块则需要设置对外暴露的props + const accessorFunctions = initProps(newSchema.schema?.properties) + + // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods + setState(newSchema.state, true) + clearNodes() + await nextTick() + setPagecss(data.css) + Object.assign(schema, newSchema) + + // 当上下文环境设置完成之后再去处理区块属性访问器的watchEffect + accessorFunctions.forEach(({ property, accessorFn, type }) => { + const propsWatchEffectKey = `${property}${type}` + propsAccessorMap.set( + propsWatchEffectKey, + watchEffect(() => { + try { + accessorFn() + } catch (error) { + globalNotify({ + type: 'warning', + title: `区块属性${property}的访问器函数:${accessorFn.name}执行报错`, + message: error?.message || `区块属性${property}的访问器函数:${accessorFn.name}执行报错,请检查语法` + }) + } + }) + ) + }) + + return schema +} + +const getNode = (id, parent) => (id ? getNodeById(id, parent) : schema) + +let canvasRenderer = null + +const defaultRenderer = function () { + // 渲染画布增加根节点,与出码和预览保持一致 + const rootChildrenSchema = { + componentName: 'div', + // 手动添加一个唯一的属性,后续在画布选中此节点时方便处理额外的逻辑。由于没有修改schema,不会影响出码 + props: { ...schema.props, 'data-id': 'root-container' }, + children: schema.children + } + + return h( + 'tiny-i18n-host', + { + locale: 'zh_CN', + key: refreshKey.value, + ref: 'page', + className: 'design-page' + }, + schema.children?.length ? h(renderer, { schema: rootChildrenSchema, parent: schema }) : [h(CanvasEmpty)] + ) +} + +const getRenderer = () => canvasRenderer || defaultRenderer + +const setRenderer = (fn) => { + canvasRenderer = fn +} + +export default { + setup() { + provide('rootSchema', schema) + + const { locale } = inject(I18nInjectionKey).global + const { data } = useBroadcastChannel({ name: BROADCAST_CHANNEL.CanvasLang }) + const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength }) + + watch(data, () => { + locale.value = data.value + }) + + watch( + () => schema?.children?.length, + (length) => { + post(length) + } + ) + + // 这里监听schema.methods,为了保证methods上下文环境始终为最新 + watch( + () => schema.methods, + (value) => { + setMethods(value, true) + }, + { + deep: true + } + ) + }, + render() { + return getRenderer().call(this) + } +} + +export const api = { + getUtils, + setUtils, + updateUtils, + deleteUtils, + getBridge, + setBridge, + getMethods, + setMethods, + setController, + setConfigure, + getSchema, + setSchema, + getState, + deleteState, + setState, + getProps, + setProps, + getContext, + getNode, + getRoot, + setPagecss, + setCondition, + getCondition, + getConditions, + getGlobalState, + getDataSourceMap, + setDataSourceMap, + setGlobalState, + setNode, + getRenderer, + setRenderer, + globalNotify, + generateFn, + getCollectionMethodsMap, + getBlockSlotDataMap, + getComponent +} diff --git a/packages/renderer/src/Renderer.js b/packages/renderer/src/Renderer.js new file mode 100644 index 000000000..b6f67a629 --- /dev/null +++ b/packages/renderer/src/Renderer.js @@ -0,0 +1,3 @@ +export default class Renderer { + constructor() {} +} diff --git a/packages/renderer/src/builtin/CanvasBox.vue b/packages/renderer/src/builtin/CanvasBox.vue new file mode 100644 index 000000000..96beea7c2 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasBox.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/renderer/src/builtin/CanvasCollection.js b/packages/renderer/src/builtin/CanvasCollection.js new file mode 100644 index 000000000..b5cb22762 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasCollection.js @@ -0,0 +1,278 @@ +/** + * 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 { getController } from '../render' +import { api } from '../RenderMain' +import { useModal } from '@opentiny/tiny-engine-meta-register' + +const NAME_PREFIX = { + loop: 'loop', + table: 'getTableData', + page: 'pageConfig', + grid: 'tinyGrid', + tree: 'tinyTree', + select: 'tinySelect' +} + +const genRemoteMethodToLifeSetup = (variableName, sourceRef, pageSchema) => { + if (sourceRef.value.data?.option) { + const setupFn = pageSchema.lifeCycles?.setup?.value + const fetchBody = ` + this.state.${variableName} = [] + this.dataSourceMap.${sourceRef.value.name}.load().then(data=>{ this.state.${variableName}=data })` + + if (!setupFn) { + pageSchema.lifeCycles = pageSchema.lifeCycles || {} + pageSchema.lifeCycles.setup = { + type: 'JSFunction', + value: `function setup({ props, state, watch, onMounted }) {${fetchBody}}` + } + } else { + pageSchema.lifeCycles.setup.value = setupFn.trim().replace(/\}$/, fetchBody + '}') + } + } +} + +const removeState = (pageSchema, variableName) => { + delete pageSchema.state[variableName] + + const { parse, traverse, generate } = getController().ast + const setupFn = pageSchema.lifeCycles?.setup?.value + + try { + const ast = parse(setupFn) + + traverse(ast, { + ExpressionStatement(path) { + path.toString().includes(variableName) && path.remove() + } + }) + + pageSchema.lifeCycles.setup.value = generate(ast).code + } catch (error) { + // do nothing + } +} + +const setStateWithSourceRef = (pageSchema, variableName, sourceRef, data) => { + api.setState({ [variableName]: data }) + pageSchema.state[variableName] = data + + if (sourceRef.value.data?.option?.isSync) { + genRemoteMethodToLifeSetup(variableName, sourceRef, pageSchema) + } +} + +const defaultHandlerTemplate = ({ node, sourceRef, schemaId, pageSchema }) => { + const genVarName = (schemaId) => `${NAME_PREFIX.loop}${schemaId}` + + const updateNode = () => { + const { configure } = getController().getMaterial(node?.componentName) + + if (!configure?.loop) { + return + } + + const variableName = genVarName(schemaId) + + setStateWithSourceRef(pageSchema, variableName, sourceRef, sourceRef.value.data?.data) + + node.loop = { + type: 'JSExpression', + value: `this.state.${variableName}` + } + } + + const clearBindVar = () => { + const variableName = genVarName(schemaId) + + removeState(pageSchema, variableName) + } + + return { + updateNode, + clearBindVar + } +} + +const generateAssginColumns = (newColumns, oldColumns) => { + newColumns.forEach((item) => { + const targetColumn = oldColumns.find((value) => value.field === item.field) + if (targetColumn) { + Object.assign(item, targetColumn) + } + }) + return newColumns +} + +const askShouldImportData = ({ node, sourceRef }) => { + useModal().confirm({ + message: '检测到表格存在配置的数据,是否需要引入?', + exec() { + const sourceColums = sourceRef.value?.data?.columns?.map(({ title, field }) => ({ title, field })) || [] + // 这里需要找到对应列,然后进行列合并 + node.props.columns = generateAssginColumns(sourceColums, node.props.columns) + }, + cancel() { + node.props.columns = [...(sourceRef.value.data?.columns || [])] + } + }) +} + +const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName }) => { + if (!node || !node.props) { + return + } + + // 如果使用了数据元则需要删除表格的data属性 + delete node?.props?.data + + if (node.props.columns.length) { + askShouldImportData({ node, sourceRef }) + } else { + node.props.columns = [...(sourceRef.value.data?.columns || [])] + } + + const pageConfig = { + attrs: { + currentPage: 1, + pageSize: 50, + pageSizes: [10, 20, 50], + total: 0, + layout: 'sizes,total, prev, pager, next, jumper' + } + } + + node.props.pager = pageConfig + + pageSchema.methods[methodName] = { + type: 'JSFunction', + value: `function ${methodName}({ page, sort, sortBy, filters}) { +/** +* @param {Object} sort 排序数据 +* @param {Array} sortBy 排序方式 +* @param {Object} page 分页数据 +* @param {Array} filters 筛选数据 +* @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } } +*/ +return new Promise((resolve, reject) => { +this.dataSourceMap.${sourceName}.load().then((res) => { + // 如果按照数据源面板的建议格式编写dataHandler + // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} } + resolve({ result: res.data.items, page: { total: res.data.total } }); +}); +}); +}` + } +} + +const extraHandlerMap = { + TinyGrid: ({ node, sourceRef, schemaId, pageSchema }) => { + const sourceName = sourceRef.value?.name + const methodName = `${NAME_PREFIX.table}${schemaId}` + + node.props.fetchData = { + type: 'JSExpression', + value: `{ api: this.${methodName} }` + } + + const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName }) + + const clearBindVar = () => { + // 当数据源组件children字段为空时,及时清空创建的methods + delete pageSchema.methods[methodName] + } + + return { + updateNode, + clearBindVar + } + }, + TinyTree: ({ node, sourceRef, schemaId, pageSchema }) => { + const genVarName = (schemaId) => `${NAME_PREFIX.tree}${schemaId}` + + const updateNode = () => { + const variableName = genVarName(schemaId) + + const arrayToTree = (data) => { + const map = {} + const tree = [] + let node = null + let i = 0 + + for (i = 0; i < data.length; i++) { + map[data[i].id] = data[i] + data[i].children = [] + } + + for (i = 0; i < data.length; i++) { + node = data[i] + if (node.pid !== '') { + map[node.pid]?.children?.push(node) + } else { + tree.push(node) + } + } + + return tree + } + + setStateWithSourceRef(pageSchema, variableName, sourceRef, arrayToTree(sourceRef.value.data?.data)) + + node.props.data = { + type: 'JSExpression', + value: `this.state.${variableName}` + } + } + + const clearBindVar = () => { + const variableName = genVarName(schemaId) + + removeState(pageSchema, variableName) + } + + return { + updateNode, + clearBindVar + } + }, + TinySelect: ({ node, sourceRef, schemaId, pageSchema }) => { + const genVarName = (schemaId) => `${NAME_PREFIX.select}${schemaId}` + + const updateNode = () => { + const variableName = genVarName(schemaId) + + setStateWithSourceRef(pageSchema, variableName, sourceRef, sourceRef.value.data?.data) + + node.props.options = { + type: 'JSExpression', + value: `this.state.${variableName}` + } + } + + const clearBindVar = () => { + const variableName = genVarName(schemaId) + + removeState(pageSchema, variableName) + } + + return { + updateNode, + clearBindVar + } + } +} + +export const getHandler = ({ node, sourceRef, schemaId, pageSchema }) => + extraHandlerMap[node.componentName] + ? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema }) + : defaultHandlerTemplate({ node, sourceRef, schemaId, pageSchema }) diff --git a/packages/renderer/src/builtin/CanvasCollection.vue b/packages/renderer/src/builtin/CanvasCollection.vue new file mode 100644 index 000000000..d033924f3 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasCollection.vue @@ -0,0 +1,106 @@ + + + diff --git a/packages/renderer/src/builtin/CanvasIcon.vue b/packages/renderer/src/builtin/CanvasIcon.vue new file mode 100644 index 000000000..42567318b --- /dev/null +++ b/packages/renderer/src/builtin/CanvasIcon.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/renderer/src/builtin/CanvasImg.vue b/packages/renderer/src/builtin/CanvasImg.vue new file mode 100644 index 000000000..2581381c6 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasImg.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/renderer/src/builtin/CanvasPlaceholder.vue b/packages/renderer/src/builtin/CanvasPlaceholder.vue new file mode 100644 index 000000000..698df4d47 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasPlaceholder.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/renderer/src/builtin/CanvasSlot.vue b/packages/renderer/src/builtin/CanvasSlot.vue new file mode 100644 index 000000000..2c5455976 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasSlot.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/renderer/src/builtin/CanvasText.vue b/packages/renderer/src/builtin/CanvasText.vue new file mode 100644 index 000000000..ec9de2992 --- /dev/null +++ b/packages/renderer/src/builtin/CanvasText.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/renderer/src/builtin/builtin.json b/packages/renderer/src/builtin/builtin.json new file mode 100644 index 000000000..ad56e67ab --- /dev/null +++ b/packages/renderer/src/builtin/builtin.json @@ -0,0 +1,540 @@ +{ + "data": { + "materials": { + "components": [ + { + "icon": "Box", + "name": { + "zh_CN": "Box" + }, + "component": "div", + "schema": { + "slots": {}, + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [] + } + ], + "events": { + "onClick": { + "label": { + "zh_CN": "点击事件" + }, + "description": { + "zh_CN": "点击时触发的回调函数" + }, + "type": "event", + "functionInfo": { + "params": [], + "returns": {} + }, + "defaultValue": "" + } + }, + "shortcuts": { + "properties": [] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "loop": true, + "isContainer": true, + "nestingRule": { + "childWhitelist": [], + "descendantBlacklist": [] + } + } + }, + { + "icon": "slot", + "name": { + "zh_CN": "Slot" + }, + "component": "Slot", + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "name", + "type": "String", + "label": { + "text": { + "zh_CN": "插槽名称" + } + }, + "cols": 12, + "widget": { + "component": "InputConfigurator", + "props": {} + } + }, + { + "property": "params", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "作用域参数" + } + }, + "widget": { + "component": "CodeConfigurator", + "props": { + "language": "json", + "tips": { + "title": { "zh_CN": "提示:数据为数组类型" }, + "demo": { + "zh_CN": "示例:\n[\n {\n \"name\": \"text\",\n \"value\": {\n \"type\": \"JSExpression\",\n \"value\": \"this.state.greetingMessage\"\n }\n },\n {\n \"name\": \"count\",\n \"value\": 1\n }\n]" + } + } + } + } + } + ] + } + ], + "events": {}, + "shortcuts": { + "properties": [] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "isContainer": true + } + }, + { + "icon": "Collection", + "name": { + "zh_CN": "Collection" + }, + "component": "Collection", + "schema": { + "slots": {}, + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "condition", + "type": "Boolean", + "defaultValue": true, + "label": { + "text": { + "zh_CN": "是否渲染" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "SwitchConfigurator", + "props": {} + } + }, + { + "property": "style", + "type": "String", + "defaultValue": "", + "label": { + "text": { + "zh_CN": "样式" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "CodeConfigurator", + "props": {} + } + }, + { + "property": "dataSource", + "type": "String", + "defaultValue": "", + "bindState": false, + "label": { + "text": { + "zh_CN": "数据源" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "CollectionConfigurator", + "props": {} + } + } + ] + } + ], + "events": {}, + "shortcuts": { + "properties": [] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "isContainer": true + } + }, + { + "icon": "Text", + "name": { + "zh_CN": "Text" + }, + "component": "Text", + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "text", + "type": "String", + "defaultValue": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。", + "label": { + "text": { + "zh_CN": "文本内容" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "InputConfigurator", + "props": { + "type": "textarea", + "autosize": true + } + } + } + ] + } + ], + "events": { + "onClick": { + "label": { + "zh_CN": "点击事件" + }, + "description": { + "zh_CN": "点击时触发的回调函数" + }, + "type": "event", + "functionInfo": { + "params": [], + "returns": {} + }, + "defaultValue": "" + } + }, + "shortcuts": { + "properties": ["text"] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "loop": true + } + }, + { + "icon": "icon", + "name": { + "zh_CN": "Icon" + }, + "component": "Icon", + "container": false, + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "name", + "type": "String", + "defaultValue": "IconDel", + "bindState": true, + "label": { + "text": { + "zh_CN": "图标类型" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "SelectIconConfigurator", + "props": {} + } + } + ] + } + ], + "events": { + "onClick": { + "label": { + "zh_CN": "点击事件" + }, + "description": { + "zh_CN": "点击时触发的回调函数" + }, + "type": "event", + "functionInfo": { + "params": [], + "returns": {} + }, + "defaultValue": "" + } + }, + "shortcuts": { + "properties": ["name"] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "loop": true + } + }, + { + "icon": "Image", + "name": { + "zh_CN": "Img" + }, + "component": "Img", + "container": false, + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "src", + "type": "String", + "defaultValue": "", + "bindState": true, + "label": { + "text": { + "zh_CN": "src路径" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "InputConfigurator", + "props": {} + } + } + ] + } + ], + "events": { + "onClick": { + "label": { + "zh_CN": "点击事件" + }, + "description": { + "zh_CN": "点击时触发的回调函数" + }, + "type": "event", + "functionInfo": { + "params": [], + "returns": {} + }, + "defaultValue": "" + } + }, + "shortcuts": { + "properties": ["src"] + }, + "contentMenu": { + "actions": [] + } + }, + "configure": { + "loop": true + } + } + ], + "snippets": [ + { + "group": "layout", + "label": { + "zh_CN": "布局与容器" + }, + "children": [ + { + "name": { + "zh_CN": "盒子容器" + }, + "screenshot": "", + "snippetName": "Box", + "icon": "Box", + "schema": { + "componentName": "div", + "props": {} + } + } + ] + }, + { + "group": "basic", + "label": { + "zh_CN": "基础元素" + }, + "children": [ + { + "name": { + "zh_CN": "文本" + }, + "screenshot": "", + "snippetName": "Text", + "icon": "Text", + "schema": { + "componentName": "Text", + "props": { + "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。" + } + } + }, + { + "name": { + "zh_CN": "图标" + }, + "screenshot": "", + "snippetName": "Icon", + "icon": "icon", + "schema": { + "componentName": "Icon", + "props": { + "name": "IconDel" + } + } + }, + { + "name": { + "zh_CN": "图片" + }, + "screenshot": "", + "snippetName": "Img", + "icon": "Image", + "schema": { + "componentName": "Img", + "props": { + "src": "https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/designer-default-icon.jpg" + } + } + } + ] + }, + { + "group": "advanced", + "label": { + "zh_CN": "高级元素" + }, + "children": [ + { + "name": { + "zh_CN": "插槽" + }, + "screenshot": "", + "snippetName": "Slot", + "icon": "slot", + "schema": { + "componentName": "Slot", + "props": {} + } + }, + { + "name": { + "zh_CN": "数据源容器" + }, + "screenshot": "", + "snippetName": "Collection", + "icon": "Collection", + "schema": { + "componentName": "Collection", + "props": {} + } + } + ] + } + ] + } + } +} diff --git a/packages/renderer/src/builtin/helper.js b/packages/renderer/src/builtin/helper.js new file mode 100644 index 000000000..59200272e --- /dev/null +++ b/packages/renderer/src/builtin/helper.js @@ -0,0 +1,46 @@ +/** + * 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 const getStyleValue = (value) => { + if (typeof value === 'number' || /^\d+\.?\d*$/.test(value)) { + return `${value}px` + } + + if (/^\d+\.?\d*(px|%|pt|em|rem|vw|vh)$/.test(value)) { + return value + } + + return '' +} + +export const alignMap = { + 'flex-start': 'flex-start', + 'flex-end': 'flex-end', + center: 'center', + stretch: 'stretch', + start: 'start', + end: 'end' +} + +export const justAlignMap = { + 'space-between': 'space-between', + 'space-around': 'space-around', + 'space-evenly': 'space-evenly', + 'flex-start': 'flex-start', + 'flex-end': 'flex-end', + stretch: 'stretch', + center: 'center', + start: 'start', + end: 'end', + left: 'left', + right: 'right' +} diff --git a/packages/renderer/src/builtin/index.js b/packages/renderer/src/builtin/index.js new file mode 100644 index 000000000..f857ff1a1 --- /dev/null +++ b/packages/renderer/src/builtin/index.js @@ -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 CanvasText from './CanvasText.vue' +import CanvasBox from './CanvasBox.vue' +import CanvasCollection from './CanvasCollection.vue' +import CanvasIcon from './CanvasIcon.vue' +import CanvasSlot from './CanvasSlot.vue' +import CanvasImg from './CanvasImg.vue' +import CanvasPlaceholder from './CanvasPlaceholder.vue' + +export { CanvasText, CanvasBox, CanvasCollection, CanvasIcon, CanvasSlot, CanvasImg, CanvasPlaceholder } diff --git a/packages/renderer/src/constants.js b/packages/renderer/src/constants.js new file mode 100644 index 000000000..9fa572a24 --- /dev/null +++ b/packages/renderer/src/constants.js @@ -0,0 +1,3 @@ +export const NODE_UID = 'data-uid' +export const NODE_TAG = 'data-tag' +export const NODE_LOOP = 'loop-id' diff --git a/packages/renderer/src/context.js b/packages/renderer/src/context.js new file mode 100644 index 000000000..710cf3ed9 --- /dev/null +++ b/packages/renderer/src/context.js @@ -0,0 +1,57 @@ +/** + * 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 } from 'vue' +import { utils } from '@opentiny/tiny-engine-utils' + +export const context = shallowReactive({}) + +// 从大纲树控制隐藏 +export const conditions = shallowReactive({}) + +const nodes = {} + +export const setNode = (schema, parent) => { + schema.id = schema.id || utils.guid() + nodes[schema.id] = { node: schema, parent } +} + +export const getNode = (id, parent) => { + return parent ? nodes[id] : nodes[id].node +} + +export const delNode = (id) => delete nodes[id] + +export const clearNodes = () => { + Object.keys(nodes).forEach(delNode) +} + +export const getRoot = (id) => { + const { parent } = getNode(id, true) + + return parent?.id ? getRoot(parent.id) : parent +} + +export const setContext = (ctx, clear) => { + clear && Object.keys(context).forEach((key) => delete context[key]) + Object.assign(context, ctx) +} + +export const getContext = () => context + +export const setCondition = (id, visible = false) => { + conditions[id] = visible +} + +export const getCondition = (id) => conditions[id] !== false + +export const getConditions = () => conditions diff --git a/packages/renderer/src/index.js b/packages/renderer/src/index.js new file mode 100644 index 000000000..77d39e99d --- /dev/null +++ b/packages/renderer/src/index.js @@ -0,0 +1,16 @@ +/** + * 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 Renderer, { api } from './RenderMain' + +export default Renderer + +export { api } diff --git a/packages/renderer/src/render.js b/packages/renderer/src/render.js new file mode 100644 index 000000000..eea363abc --- /dev/null +++ b/packages/renderer/src/render.js @@ -0,0 +1,733 @@ +/** + * 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, reactive } from 'vue' +import { isHTMLTag, hyphenate } from '@vue/shared' +import { useBroadcastChannel } from '@vueuse/core' +import { constants, utils } from '@opentiny/tiny-engine-utils' +import babelPluginJSX from '@vue/babel-plugin-jsx' +import { transformSync } from '@babel/core' +import i18nHost from '@opentiny/tiny-engine-i18n-host' +import { CanvasRow, CanvasCol, CanvasRowColContainer } from '@opentiny/tiny-engine-builtin-component' + +import { NODE_UID as DESIGN_UIDKEY, NODE_TAG as DESIGN_TAGKEY, NODE_LOOP as DESIGN_LOOPID } from './constants' +import { context, conditions, setNode } from './context' +import { + CanvasBox, + CanvasCollection, + CanvasIcon, + CanvasText, + CanvasSlot, + CanvasImg, + CanvasPlaceholder +} from './builtin' + +const { BROADCAST_CHANNEL } = constants +const { hyphenateRE } = utils +const customElements = {} + +const transformJSX = (code) => { + const res = transformSync(code, { + plugins: [ + [ + babelPluginJSX, + { + pragma: 'h', + isCustomElement: (name) => customElements[name] + } + ] + ] + }) + return (res.code || '') + .replace(/import \{.+\} from "vue";/, '') + .replace(/h\(_?resolveComponent\((.*?)\)/g, `h(this.getComponent($1)`) + .replace(/_?resolveComponent/g, 'h') + .replace(/_?createTextVNode\((.*?)\)/g, '$1') + .trim() +} + +export const blockSlotDataMap = reactive({}) + +export const getBlockSlotDataMap = () => blockSlotDataMap + +const Mapper = { + Icon: CanvasIcon, + Text: CanvasText, + Collection: CanvasCollection, + div: CanvasBox, + Slot: CanvasSlot, + slot: CanvasSlot, + Template: CanvasBox, + Img: CanvasImg, + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasPlaceholder +} + +const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify }) + +// 此处向外层window传递notify配置参数 +export const globalNotify = (options) => post(options) + +export const collectionMethodsMap = {} + +export const getCollectionMethodsMap = () => collectionMethodsMap + +const getNative = (name) => { + return window.TinyLowcodeComponent?.[name] +} + +const getBlock = (name) => { + return window.blocks?.[name] +} + +const configure = {} +const controller = {} + +export const setConfigure = (configureData) => { + Object.assign(configure, configureData) +} + +export function setController(controllerData) { + Object.assign(controller, controllerData) +} + +export const getController = () => controller + +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 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 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 renderDefault = (children, scope, parent) => + children.map?.((child) => + // eslint-disable-next-line no-use-before-define + h(renderer, { + schema: child, + scope, + parent + }) + ) + +const parseJSSlot = (data, scope) => { + return ($scope) => renderDefault(data.value, { ...scope, ...$scope }, data) +} + +export const generateFn = (innerFn, context) => { + return (...args) => { + // 如果有数据源标识,则表格的fetchData返回数据源的静态数据 + const sourceId = 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) { + globalNotify({ + type: 'warning', + title: `函数:${innerFn.name}执行报错`, + message: error?.message || `函数:${innerFn.name}执行报错,请检查语法` + }) + } + + // 这里注意如果innerFn返回的是一个promise则需要捕获异常,重新返回默认一条空数据 + if (result.then) { + result = new Promise((resolve) => { + result.then(resolve).catch((error) => { + globalNotify({ + type: 'warning', + title: '异步函数执行报错', + message: error?.message || '异步函数执行报错,请检查语法' + }) + // 这里需要至少返回一条空数据,方便用户使用表格默认插槽 + resolve({ + result: [{}], + page: { total: 1 } + }) + }) + }) + } + + return result + } + } +} + +// 解析函数字符串结构 +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 +} + +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 generateBlockContent = (schema) => { + if (schema?.componentName === 'Collection') { + generateCollection(schema) + } + if (Array.isArray(schema?.children)) { + schema.children.forEach((item) => { + generateBlockContent(item) + }) + } +} + +const registerBlock = (componentName) => { + getController() + .registerBlock?.(componentName) + .then((res) => { + const blockSchema = res.content + + // 拿到区块数据,建立区块中数据源的映射关系 + generateBlockContent(blockSchema) + + // 如果区块的根节点有百分比高度,则需要特殊处理,把高度百分比传递下去,适配大屏应用 + if (/height:\s*?[\d|.]+?%/.test(blockSchema?.props?.style)) { + const blockDoms = document.querySelectorAll(hyphenate(componentName)) + blockDoms.forEach((item) => { + item.style.height = '100%' + }) + } + }) +} + +export const wrapCustomElement = (componentName) => { + const material = getController().getMaterial(componentName) + + if (!Object.keys(material).length) { + registerBlock(componentName) + } + + customElements[componentName] = { + name: componentName + '.ce', + render() { + return h( + hyphenate(componentName), + window.parent.TinyGlobalConfig.dslMode === 'Vue' ? getPlainProps(this.$attrs) : this.$attrs, + this.$slots.default?.() + ) + } + } + + return customElements[componentName] +} + +export const getComponent = (name) => { + return ( + Mapper[name] || + getNative(name) || + getBlock(name) || + customElements[name] || + (isHTMLTag(name) ? name : wrapCustomElement(name)) + ) +} + +// 解析JSX字符串为可执行函数 +const parseJSXFunction = (data, 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 + }) + } catch (error) { + globalNotify({ + type: 'warning', + title: '函数声明解析报错', + message: error?.message || '函数声明解析报错,请检查语法' + }) + + return newFn() + } +} + +const parseJSFunction = (data, scope, ctx = context) => { + try { + const innerFn = newFn(`return ${data.value}`).bind(ctx)() + return generateFn(innerFn, ctx) + } catch (error) { + return parseJSXFunction(data, ctx) + } +} + +const parseList = [] + +export function parseData(data, scope, ctx = context) { + let res = data + parseList.some((item) => { + if (item.type(data)) { + res = item.parseFunc(data, scope, ctx) + + return true + } + + return false + }) + + return res +} + +const parseCondition = (condition, scope, ctx = context) => { + // eslint-disable-next-line no-eq-null + return condition == null ? true : parseData(condition, scope, ctx) +} + +const parseLoopArgs = (_loop) => { + if (_loop) { + const { item, index, loopArgs = '' } = _loop + const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }` + return newFn('item,index', body)(item, index) + } + return undefined +} + +export const getIcon = (name) => window.TinyVueIcon?.[name]?.() || '' + +const parseObjectData = (data, scope, ctx) => { + if (!data) { + return data + } + + // 如果是状态访问器,则直接解析默认值 + if (isStateAccessor(data)) { + return parseData(data.defaultValue) + } + + // 解析通过属性传递icon图标组件 + if (data.componentName === 'Icon') { + return getIcon(data.props.name) + } + const res = {} + Object.entries(data).forEach(([key, value]) => { + // 如果是插槽则需要进行特殊处理 + if (key === 'slot' && value?.name) { + res[key] = value.name + } else { + res[key] = parseData(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: isString, + parseFunc: parseString + }, + { + type: isArray, + parseFunc: parseArray + }, + { + type: isFunction, + parseFunc: parseFunction + }, + { + type: isObject, + parseFunc: parseObjectData + } + ] +) + +const stopEvent = (event) => { + event.preventDefault?.() + event.stopPropagation?.() + return false +} + +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 + + 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) => { + 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 }, currentSlot.parent) + }) + + return slots + } + + return { default: () => renderDefault(children, scope, schema) } +} + +const checkGroup = (componentName) => configure[componentName]?.nestingRule?.childWhitelist?.length + +const clickCapture = (componentName) => configure[componentName]?.clickCapture !== false + +const getBindProps = (schema, scope) => { + const { id, componentName } = schema + const invalidity = configure[componentName]?.invalidity || [] + + if (componentName === 'CanvasPlaceholder') { + return {} + } + + const bindProps = { + ...parseData(schema.props, scope), + [DESIGN_UIDKEY]: id, + [DESIGN_TAGKEY]: componentName, + onMouseover: stopEvent, + onFocus: stopEvent + } + if (scope) { + bindProps[DESIGN_LOOPID] = scope.index === undefined ? scope.idx : scope.index + } + + // 在捕获阶段阻止事件的传播 + if (clickCapture(componentName)) { + bindProps.onClickCapture = stopEvent + } + + if (Mapper[componentName]) { + bindProps.schema = schema + } + + // 绑定组件属性时需要将 className 重命名为 class,防止覆盖组件内置 class + bindProps.class = bindProps.className + delete bindProps.className + + // 使画布中元素可拖拽 + bindProps.draggable = true + + // 过滤在门户网站上配置的画布丢弃的属性 + invalidity.forEach((prop) => delete bindProps[prop]) + + 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, parent) => { + return children.map?.((schema) => { + const { componentName, children, loop, loopArgs, condition, id } = schema + const loopList = parseData(loop, scope) + + const renderElement = (item, index) => { + const mergeScope = getLoopScope({ + scope, + index, + item, + loopArgs + }) + + setNode(schema, parent) + + if (conditions[id] === false || !parseCondition(condition, mergeScope)) { + return null + } + + const renderChildren = injectPlaceHolder(componentName, children) + + return h( + getComponent(componentName), + getBindProps(schema, mergeScope), + Array.isArray(renderChildren) + ? renderSlot(renderChildren, mergeScope, schema) + : parseData(renderChildren, mergeScope) + ) + } + + return loopList?.length ? loopList.map(renderElement) : renderElement() + }) +} + +const getChildren = (schema, mergeScope) => { + const { componentName, children } = schema + const renderChildren = injectPlaceHolder(componentName, children) + + const component = getComponent(componentName) + const isNative = typeof component === 'string' + const isCustomElm = customElements[componentName] + const isGroup = checkGroup(componentName) + + if (Array.isArray(renderChildren)) { + if (isNative || isCustomElm) { + return renderDefault(renderChildren, mergeScope, schema) + } else { + return isGroup + ? renderGroup(renderChildren, mergeScope, schema) + : renderSlot(renderChildren, mergeScope, schema, isCustomElm) + } + } else { + return parseData(renderChildren, mergeScope) + } +} + +export const renderer = { + name: 'renderer', + props: { + schema: Object, + scope: Object, + parent: Object + }, + setup(props) { + provide('schema', props.schema) + }, + render() { + const { scope, schema, parent } = this + const { componentName, loop, loopArgs, condition } = schema + + // 处理数据源和表格fetchData的映射关系 + generateCollection(schema) + + if (!componentName) { + return parseData(schema, scope) + } + + const component = getComponent(componentName) + + const loopList = parseData(loop, scope) + + const renderElement = (item, index) => { + let mergeScope = item + ? getLoopScope({ + item, + index, + loopArgs, + scope + }) + : scope + + // 如果是区块,并且使用了区块的作用域插槽,则需要将作用域插槽的数据传递下去 + if (parent?.componentType === 'Block' && componentName === 'Template' && schema.props?.slot?.params?.length) { + const slotName = schema.props.slot?.name || schema.props.slot + const blockName = parent.componentName + const slotData = blockSlotDataMap[blockName]?.[slotName] || {} + mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData + } + + // 给每个节点设置schema.id,并缓存起来 + setNode(schema, parent) + + if (conditions[schema.id] === false || !parseCondition(condition, mergeScope)) { + return null + } + + return h(component, getBindProps(schema, mergeScope), getChildren(schema, mergeScope)) + } + + return loopList?.length ? loopList.map(renderElement) : renderElement() + } +} + +export default renderer diff --git a/packages/renderer/vite.config.js b/packages/renderer/vite.config.js new file mode 100644 index 000000000..7392ee722 --- /dev/null +++ b/packages/renderer/vite.config.js @@ -0,0 +1,51 @@ +/** + * 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 generateComments from '@opentiny/tiny-engine-vite-plugin-meta-comments' +import { vitePluginBuildEntry } from './scripts/vite-plugin-separate-build' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: './', + + plugins: [ + vue(), + vueJsx(), + generateComments(), + vitePluginBuildEntry({ + canvas: path.resolve(__dirname, './scripts/canvas-vite.config.js') + }) + ], + publicDir: false, + build: { + cssCodeSplit: true, + lib: { + entry: { + index: path.resolve(__dirname, './index.js') + }, + formats: ['es'] + }, + rollupOptions: { + output: { + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + assetFileNames: '[name].[ext]' + }, + external: ['vue', '@vueuse/core', 'vue-i18n', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/] + }, + minify: true + } +}) From e12ae04204eb6a2242275bf9abcac088d885f958 Mon Sep 17 00:00:00 2001 From: yy-wow Date: Thu, 24 Oct 2024 02:30:21 -0700 Subject: [PATCH 02/26] fix: miss file --- packages/canvas/render/src/CanvasEmpty.vue | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/canvas/render/src/CanvasEmpty.vue diff --git a/packages/canvas/render/src/CanvasEmpty.vue b/packages/canvas/render/src/CanvasEmpty.vue new file mode 100644 index 000000000..c6dc2c454 --- /dev/null +++ b/packages/canvas/render/src/CanvasEmpty.vue @@ -0,0 +1,14 @@ + + + From 80b514ef16ef0e7684175b3ac6ce0b988a9d190f Mon Sep 17 00:00:00 2001 From: yy-wow Date: Tue, 29 Oct 2024 18:36:54 -0700 Subject: [PATCH 03/26] refactor: split renderer package --- packages/canvas/container/src/container.js | 2 +- packages/canvas/render/src/RenderMain.js | 455 ----------- .../canvas/render/src/builtin/CanvasBox.vue | 22 - .../render/src/builtin/CanvasCollection.js | 278 ------- .../render/src/builtin/CanvasCollection.vue | 106 --- .../canvas/render/src/builtin/CanvasIcon.vue | 30 - .../canvas/render/src/builtin/CanvasImg.vue | 18 - .../render/src/builtin/CanvasPlaceholder.vue | 26 - .../canvas/render/src/builtin/CanvasSlot.vue | 22 - .../canvas/render/src/builtin/CanvasText.vue | 18 - .../canvas/render/src/builtin/builtin.json | 540 ------------- packages/canvas/render/src/builtin/helper.js | 46 -- packages/canvas/render/src/builtin/index.js | 21 - packages/canvas/render/src/context.js | 57 -- packages/canvas/render/src/lowcode.js | 7 +- packages/canvas/render/src/render.js | 729 ------------------ packages/canvas/render/src/runner.js | 5 +- packages/canvas/render/src/supportUmdBlock.js | 6 +- packages/renderer/src/RenderMain.js | 20 +- .../renderer/src/builtin/CanvasCollection.js | 7 +- .../renderer/src/builtin/CanvasCollection.vue | 4 +- packages/renderer/src/context.js | 7 + packages/renderer/src/index.js | 3 +- packages/renderer/src/render.js | 14 +- 24 files changed, 53 insertions(+), 2390 deletions(-) delete mode 100644 packages/canvas/render/src/RenderMain.js delete mode 100644 packages/canvas/render/src/builtin/CanvasBox.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasCollection.js delete mode 100644 packages/canvas/render/src/builtin/CanvasCollection.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasIcon.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasImg.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasPlaceholder.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasSlot.vue delete mode 100644 packages/canvas/render/src/builtin/CanvasText.vue delete mode 100644 packages/canvas/render/src/builtin/builtin.json delete mode 100644 packages/canvas/render/src/builtin/helper.js delete mode 100644 packages/canvas/render/src/builtin/index.js delete mode 100644 packages/canvas/render/src/context.js delete mode 100644 packages/canvas/render/src/render.js diff --git a/packages/canvas/container/src/container.js b/packages/canvas/container/src/container.js index 1bf2a9238..5b3baea29 100644 --- a/packages/canvas/container/src/container.js +++ b/packages/canvas/container/src/container.js @@ -21,7 +21,7 @@ import { } from '../../common' import { useCanvas, useLayout, useResource, useTranslate, useMaterial } from '@opentiny/tiny-engine-meta-register' import { isVsCodeEnv } from '@opentiny/tiny-engine-common/js/environments' -import Builtin from '../../render/src/builtin/builtin.json' //TODO 画布内外应该分开 +import { Builtin } from '@opentiny/tiny-engine-renderer' export const POSITION = Object.freeze({ TOP: 'top', diff --git a/packages/canvas/render/src/RenderMain.js b/packages/canvas/render/src/RenderMain.js deleted file mode 100644 index 04e2c928f..000000000 --- a/packages/canvas/render/src/RenderMain.js +++ /dev/null @@ -1,455 +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. - * - */ - -import { h, provide, inject, nextTick, shallowReactive, reactive, ref, watch, watchEffect } from 'vue' -import { I18nInjectionKey } from 'vue-i18n' -import TinyVue from '@opentiny/vue' -import * as TinyVueIcon from '@opentiny/vue-icon' -import { useBroadcastChannel } from '@vueuse/core' -import { constants, utils as commonUtils } from '@opentiny/tiny-engine-utils' -import renderer, { parseData, setConfigure, setController, globalNotify, isStateAccessor } from './render' -import { - getNode as getNodeById, - clearNodes, - getRoot, - setContext, - getContext, - setCondition, - getCondition, - getConditions, - context, - setNode -} from './context' -import CanvasEmpty from './CanvasEmpty.vue' - -const { BROADCAST_CHANNEL } = constants -const { parseFunction: generateFunction } = commonUtils - -const reset = (obj) => { - Object.keys(obj).forEach((key) => delete obj[key]) -} - -const refreshKey = ref(0) -const methods = {} -const schema = reactive({}) -const state = shallowReactive({}) -const bridge = {} -const utils = {} -const props = {} - -const globalState = ref([]) -const stores = shallowReactive({}) -const dataSourceMap = shallowReactive({}) - -watchEffect(() => { - reset(stores) - globalState.value.forEach(({ id, state = {}, getters = {} }) => { - const computedGetters = Object.keys(getters).reduce( - (acc, key) => ({ - ...acc, - [key]: parseData(getters[key], state, acc) - }), - {} - ) - - stores[id] = { ...state, ...computedGetters } - }) -}) - -const getUtils = () => utils - -const setUtils = (data, clear, isForceRefresh) => { - if (clear) { - reset(utils) - } - const utilsCollection = {} - // 目前画布还不具备远程加载utils工具类的功能,目前只能加载TinyVue组件库中的组件工具 - data?.forEach((item) => { - const util = TinyVue[item.content.exportName] - if (util) { - utilsCollection[item.name] = util - } - - // 此处需要把工具类中的icon图标也加入utils上下文环境 - const utilIcon = TinyVueIcon[item.content.exportName] - if (utilIcon) { - utilsCollection[item.name] = utilIcon - } - - // 解析函数式的工具类 - if (item.type === 'function') { - const defaultFn = () => {} - utilsCollection[item.name] = generateFunction(item.content.value, context) || defaultFn - } - }) - Object.assign(utils, utilsCollection) - - // 因为工具类并不具有响应式行为,所以需要通过修改key来强制刷新画布 - if (isForceRefresh) { - refreshKey.value++ - } -} - -const updateUtils = (data) => { - setUtils(data, false, true) -} - -const deleteUtils = (data) => { - data?.forEach((item) => { - if (utils[item.name]) { - delete utils[item.name] - } - }) - setUtils([], false, true) -} - -const setBridge = (data, clear) => { - clear && reset(bridge) - Object.assign(bridge, data) -} - -const getBridge = () => bridge - -const getMethods = () => methods - -const setMethods = (data = {}, clear) => { - clear && reset(methods) - // 这里有些方法在画布还是有执行的必要的,比如说表格的renderer和formatText方法,包括一些自定义渲染函数 - Object.assign( - methods, - Object.fromEntries( - Object.keys(data).map((key) => { - return [key, parseData(data[key], {}, getContext())] - }) - ) - ) - setContext(methods) -} - -const getState = () => state - -const deleteState = (variable) => { - delete state[variable] -} - -const generateAccessor = (type, accessor, property) => { - const accessorFn = generateFunction(accessor[type].value, context) - - return { property, accessorFn, type } -} - -// 这里缓存状态变量对应的访问器,用于watchEffect更新和取消监听 -const stateAccessorMap = new Map() - -// 缓存区块属性的访问器 -const propsAccessorMap = new Map() - -const generateStateAccessors = (type, accessor, key) => { - const stateWatchEffectKey = `${key}${type}` - const { property, accessorFn } = generateAccessor(type, accessor, key) - - // 将之前已有的watchEffect取消监听,这里操作很有必要,不然会造成数据混乱 - stateAccessorMap.get(stateWatchEffectKey)?.() - - // 更新watchEffect监听 - stateAccessorMap.set( - stateWatchEffectKey, - watchEffect(() => { - try { - accessorFn() - } catch (error) { - globalNotify({ - type: 'warning', - title: `状态变量${property}的访问器函数:${accessorFn.name}执行报错`, - message: error?.message || `状态变量${property}的访问器函数:${accessorFn.name}执行报错,请检查语法` - }) - } - }) - ) -} - -const setState = (data, clear) => { - clear && reset(state) - if (!schema.state) { - schema.state = data - } - - Object.assign(state, parseData(data, {}, getContext()) || {}) - - // 在状态变量合并之后,执行访问器中watchEffect,为了可以在访问器函数中可以访问其他state变量 - Object.entries(data || {})?.forEach(([key, stateData]) => { - if (isStateAccessor(stateData)) { - const accessor = stateData.accessor - if (accessor?.getter?.value) { - generateStateAccessors('getter', accessor, key) - } - - if (accessor?.setter?.value) { - generateStateAccessors('setter', accessor, key) - } - } - }) -} - -const getDataSourceMap = () => { - return dataSourceMap.value -} - -const setDataSourceMap = (list) => { - dataSourceMap.value = list.reduce((dMap, config) => { - const dataSource = { config: config.data } - - const result = { - code: '', - msg: 'success', - data: {} - } - result.data = - dataSource.config.type === 'array' - ? { items: dataSource?.config?.data, total: dataSource?.config?.data?.length } - : dataSource?.config?.data - - dataSource.load = () => Promise.resolve(result) - dMap[config.name] = dataSource - - return dMap - }, {}) -} - -const getGlobalState = () => { - return globalState.value -} - -const setGlobalState = (data = []) => { - globalState.value = data -} - -const setProps = (data, clear) => { - clear && reset(props) - Object.assign(props, data) -} - -const getProps = () => props - -const initProps = (properties = []) => { - const props = {} - const accessorFunctions = [] - - properties.forEach(({ content = [] }) => { - content.forEach(({ defaultValue, property, accessor }) => { - // 如果没有设置defaultValue就是undefined这和vue处理方式一样 - props[property] = defaultValue - - // 如果区块属性有访问器accessor,则先解析getter和setter函数 - if (accessor?.getter?.value) { - // 此处不能直接执行watchEffect,需要在上下文环境设置好之后去执行,此处只是收集函数 - accessorFunctions.push(generateAccessor('getter', accessor, property)) - } - - if (accessor?.setter?.value) { - accessorFunctions.push(generateAccessor('setter', accessor, property)) - } - }) - }) - - setProps(props, true) - - return accessorFunctions -} - -const getSchema = () => schema - -const setPagecss = (css = '') => { - const id = 'page-css' - let element = document.getElementById(id) - const head = document.querySelector('head') - - document.body.setAttribute('style', '') - - if (!element) { - element = document.createElement('style') - element.setAttribute('type', 'text/css') - element.setAttribute('id', id) - - element.innerHTML = css - head.appendChild(element) - } else { - element.innerHTML = css - } -} - -const setSchema = async (data) => { - const newSchema = JSON.parse(JSON.stringify(data || schema)) - reset(schema) - // 页面初始化的时候取消所有状态变量的watchEffect监听 - stateAccessorMap.forEach((stateAccessorFn) => { - stateAccessorFn() - }) - - // 区块初始化的时候取消所有的区块属性watchEffect监听 - propsAccessorMap.forEach((propsAccessorFn) => { - propsAccessorFn() - }) - - // 清空存状态变量和区块props访问器的缓存 - stateAccessorMap.clear() - propsAccessorMap.clear() - - const context = { - utils, - bridge, - stores, - state, - props, - dataSourceMap: {}, - emit: () => {} // 兼容访问器中getter和setter中this.emit写法 - } - Object.defineProperty(context, 'dataSourceMap', { - get: getDataSourceMap - }) - // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 - setContext(context, true) - - // 设置方法调用上下文 - setMethods(newSchema.methods, true) - - // 如果是区块则需要设置对外暴露的props - const accessorFunctions = initProps(newSchema.schema?.properties) - - // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods - setState(newSchema.state, true) - clearNodes() - await nextTick() - setPagecss(data.css) - Object.assign(schema, newSchema) - - // 当上下文环境设置完成之后再去处理区块属性访问器的watchEffect - accessorFunctions.forEach(({ property, accessorFn, type }) => { - const propsWatchEffectKey = `${property}${type}` - propsAccessorMap.set( - propsWatchEffectKey, - watchEffect(() => { - try { - accessorFn() - } catch (error) { - globalNotify({ - type: 'warning', - title: `区块属性${property}的访问器函数:${accessorFn.name}执行报错`, - message: error?.message || `区块属性${property}的访问器函数:${accessorFn.name}执行报错,请检查语法` - }) - } - }) - ) - }) - - return schema -} - -const getNode = (id, parent) => (id ? getNodeById(id, parent) : schema) - -let canvasRenderer = null - -const defaultRenderer = function () { - // 渲染画布增加根节点,与出码和预览保持一致 - const rootChildrenSchema = { - componentName: 'div', - // 手动添加一个唯一的属性,后续在画布选中此节点时方便处理额外的逻辑。由于没有修改schema,不会影响出码 - props: { ...schema.props, 'data-id': 'root-container' }, - children: schema.children - } - - return h( - 'tiny-i18n-host', - { - locale: 'zh_CN', - key: refreshKey.value, - ref: 'page', - className: 'design-page' - }, - schema.children?.length ? h(renderer, { schema: rootChildrenSchema, parent: schema }) : [h(CanvasEmpty)] - ) -} - -const getRenderer = () => canvasRenderer || defaultRenderer - -const setRenderer = (fn) => { - canvasRenderer = fn -} - -export default { - setup() { - provide('rootSchema', schema) - - const { locale } = inject(I18nInjectionKey).global - const { data } = useBroadcastChannel({ name: BROADCAST_CHANNEL.CanvasLang }) - const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength }) - - watch(data, () => { - locale.value = data.value - }) - - watch( - () => schema?.children?.length, - (length) => { - post(length) - } - ) - - // 这里监听schema.methods,为了保证methods上下文环境始终为最新 - watch( - () => schema.methods, - (value) => { - setMethods(value, true) - }, - { - deep: true - } - ) - }, - render() { - return getRenderer().call(this) - } -} - -export const api = { - getUtils, - setUtils, - updateUtils, - deleteUtils, - getBridge, - setBridge, - getMethods, - setMethods, - setController, - setConfigure, - getSchema, - setSchema, - getState, - deleteState, - setState, - getProps, - setProps, - getContext, - getNode, - getRoot, - setPagecss, - setCondition, - getCondition, - getConditions, - getGlobalState, - getDataSourceMap, - setDataSourceMap, - setGlobalState, - setNode, - getRenderer, - setRenderer -} diff --git a/packages/canvas/render/src/builtin/CanvasBox.vue b/packages/canvas/render/src/builtin/CanvasBox.vue deleted file mode 100644 index 96beea7c2..000000000 --- a/packages/canvas/render/src/builtin/CanvasBox.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/CanvasCollection.js b/packages/canvas/render/src/builtin/CanvasCollection.js deleted file mode 100644 index b5cb22762..000000000 --- a/packages/canvas/render/src/builtin/CanvasCollection.js +++ /dev/null @@ -1,278 +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. - * - */ - -import { getController } from '../render' -import { api } from '../RenderMain' -import { useModal } from '@opentiny/tiny-engine-meta-register' - -const NAME_PREFIX = { - loop: 'loop', - table: 'getTableData', - page: 'pageConfig', - grid: 'tinyGrid', - tree: 'tinyTree', - select: 'tinySelect' -} - -const genRemoteMethodToLifeSetup = (variableName, sourceRef, pageSchema) => { - if (sourceRef.value.data?.option) { - const setupFn = pageSchema.lifeCycles?.setup?.value - const fetchBody = ` - this.state.${variableName} = [] - this.dataSourceMap.${sourceRef.value.name}.load().then(data=>{ this.state.${variableName}=data })` - - if (!setupFn) { - pageSchema.lifeCycles = pageSchema.lifeCycles || {} - pageSchema.lifeCycles.setup = { - type: 'JSFunction', - value: `function setup({ props, state, watch, onMounted }) {${fetchBody}}` - } - } else { - pageSchema.lifeCycles.setup.value = setupFn.trim().replace(/\}$/, fetchBody + '}') - } - } -} - -const removeState = (pageSchema, variableName) => { - delete pageSchema.state[variableName] - - const { parse, traverse, generate } = getController().ast - const setupFn = pageSchema.lifeCycles?.setup?.value - - try { - const ast = parse(setupFn) - - traverse(ast, { - ExpressionStatement(path) { - path.toString().includes(variableName) && path.remove() - } - }) - - pageSchema.lifeCycles.setup.value = generate(ast).code - } catch (error) { - // do nothing - } -} - -const setStateWithSourceRef = (pageSchema, variableName, sourceRef, data) => { - api.setState({ [variableName]: data }) - pageSchema.state[variableName] = data - - if (sourceRef.value.data?.option?.isSync) { - genRemoteMethodToLifeSetup(variableName, sourceRef, pageSchema) - } -} - -const defaultHandlerTemplate = ({ node, sourceRef, schemaId, pageSchema }) => { - const genVarName = (schemaId) => `${NAME_PREFIX.loop}${schemaId}` - - const updateNode = () => { - const { configure } = getController().getMaterial(node?.componentName) - - if (!configure?.loop) { - return - } - - const variableName = genVarName(schemaId) - - setStateWithSourceRef(pageSchema, variableName, sourceRef, sourceRef.value.data?.data) - - node.loop = { - type: 'JSExpression', - value: `this.state.${variableName}` - } - } - - const clearBindVar = () => { - const variableName = genVarName(schemaId) - - removeState(pageSchema, variableName) - } - - return { - updateNode, - clearBindVar - } -} - -const generateAssginColumns = (newColumns, oldColumns) => { - newColumns.forEach((item) => { - const targetColumn = oldColumns.find((value) => value.field === item.field) - if (targetColumn) { - Object.assign(item, targetColumn) - } - }) - return newColumns -} - -const askShouldImportData = ({ node, sourceRef }) => { - useModal().confirm({ - message: '检测到表格存在配置的数据,是否需要引入?', - exec() { - const sourceColums = sourceRef.value?.data?.columns?.map(({ title, field }) => ({ title, field })) || [] - // 这里需要找到对应列,然后进行列合并 - node.props.columns = generateAssginColumns(sourceColums, node.props.columns) - }, - cancel() { - node.props.columns = [...(sourceRef.value.data?.columns || [])] - } - }) -} - -const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName }) => { - if (!node || !node.props) { - return - } - - // 如果使用了数据元则需要删除表格的data属性 - delete node?.props?.data - - if (node.props.columns.length) { - askShouldImportData({ node, sourceRef }) - } else { - node.props.columns = [...(sourceRef.value.data?.columns || [])] - } - - const pageConfig = { - attrs: { - currentPage: 1, - pageSize: 50, - pageSizes: [10, 20, 50], - total: 0, - layout: 'sizes,total, prev, pager, next, jumper' - } - } - - node.props.pager = pageConfig - - pageSchema.methods[methodName] = { - type: 'JSFunction', - value: `function ${methodName}({ page, sort, sortBy, filters}) { -/** -* @param {Object} sort 排序数据 -* @param {Array} sortBy 排序方式 -* @param {Object} page 分页数据 -* @param {Array} filters 筛选数据 -* @returns {Object} 返回一个promise对象,并且resolve格式为{ result: Array, page: { total: Number } } -*/ -return new Promise((resolve, reject) => { -this.dataSourceMap.${sourceName}.load().then((res) => { - // 如果按照数据源面板的建议格式编写dataHandler - // 那么dataSourceMap的res格式应该是:{ code: string, msg: string, data: {items: any[], total: number} } - resolve({ result: res.data.items, page: { total: res.data.total } }); -}); -}); -}` - } -} - -const extraHandlerMap = { - TinyGrid: ({ node, sourceRef, schemaId, pageSchema }) => { - const sourceName = sourceRef.value?.name - const methodName = `${NAME_PREFIX.table}${schemaId}` - - node.props.fetchData = { - type: 'JSExpression', - value: `{ api: this.${methodName} }` - } - - const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName }) - - const clearBindVar = () => { - // 当数据源组件children字段为空时,及时清空创建的methods - delete pageSchema.methods[methodName] - } - - return { - updateNode, - clearBindVar - } - }, - TinyTree: ({ node, sourceRef, schemaId, pageSchema }) => { - const genVarName = (schemaId) => `${NAME_PREFIX.tree}${schemaId}` - - const updateNode = () => { - const variableName = genVarName(schemaId) - - const arrayToTree = (data) => { - const map = {} - const tree = [] - let node = null - let i = 0 - - for (i = 0; i < data.length; i++) { - map[data[i].id] = data[i] - data[i].children = [] - } - - for (i = 0; i < data.length; i++) { - node = data[i] - if (node.pid !== '') { - map[node.pid]?.children?.push(node) - } else { - tree.push(node) - } - } - - return tree - } - - setStateWithSourceRef(pageSchema, variableName, sourceRef, arrayToTree(sourceRef.value.data?.data)) - - node.props.data = { - type: 'JSExpression', - value: `this.state.${variableName}` - } - } - - const clearBindVar = () => { - const variableName = genVarName(schemaId) - - removeState(pageSchema, variableName) - } - - return { - updateNode, - clearBindVar - } - }, - TinySelect: ({ node, sourceRef, schemaId, pageSchema }) => { - const genVarName = (schemaId) => `${NAME_PREFIX.select}${schemaId}` - - const updateNode = () => { - const variableName = genVarName(schemaId) - - setStateWithSourceRef(pageSchema, variableName, sourceRef, sourceRef.value.data?.data) - - node.props.options = { - type: 'JSExpression', - value: `this.state.${variableName}` - } - } - - const clearBindVar = () => { - const variableName = genVarName(schemaId) - - removeState(pageSchema, variableName) - } - - return { - updateNode, - clearBindVar - } - } -} - -export const getHandler = ({ node, sourceRef, schemaId, pageSchema }) => - extraHandlerMap[node.componentName] - ? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema }) - : defaultHandlerTemplate({ node, sourceRef, schemaId, pageSchema }) diff --git a/packages/canvas/render/src/builtin/CanvasCollection.vue b/packages/canvas/render/src/builtin/CanvasCollection.vue deleted file mode 100644 index d033924f3..000000000 --- a/packages/canvas/render/src/builtin/CanvasCollection.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/CanvasIcon.vue b/packages/canvas/render/src/builtin/CanvasIcon.vue deleted file mode 100644 index 42567318b..000000000 --- a/packages/canvas/render/src/builtin/CanvasIcon.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/CanvasImg.vue b/packages/canvas/render/src/builtin/CanvasImg.vue deleted file mode 100644 index 2581381c6..000000000 --- a/packages/canvas/render/src/builtin/CanvasImg.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/CanvasPlaceholder.vue b/packages/canvas/render/src/builtin/CanvasPlaceholder.vue deleted file mode 100644 index 698df4d47..000000000 --- a/packages/canvas/render/src/builtin/CanvasPlaceholder.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/packages/canvas/render/src/builtin/CanvasSlot.vue b/packages/canvas/render/src/builtin/CanvasSlot.vue deleted file mode 100644 index 2c5455976..000000000 --- a/packages/canvas/render/src/builtin/CanvasSlot.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/CanvasText.vue b/packages/canvas/render/src/builtin/CanvasText.vue deleted file mode 100644 index ec9de2992..000000000 --- a/packages/canvas/render/src/builtin/CanvasText.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/packages/canvas/render/src/builtin/builtin.json b/packages/canvas/render/src/builtin/builtin.json deleted file mode 100644 index ad56e67ab..000000000 --- a/packages/canvas/render/src/builtin/builtin.json +++ /dev/null @@ -1,540 +0,0 @@ -{ - "data": { - "materials": { - "components": [ - { - "icon": "Box", - "name": { - "zh_CN": "Box" - }, - "component": "div", - "schema": { - "slots": {}, - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [] - } - ], - "events": { - "onClick": { - "label": { - "zh_CN": "点击事件" - }, - "description": { - "zh_CN": "点击时触发的回调函数" - }, - "type": "event", - "functionInfo": { - "params": [], - "returns": {} - }, - "defaultValue": "" - } - }, - "shortcuts": { - "properties": [] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "loop": true, - "isContainer": true, - "nestingRule": { - "childWhitelist": [], - "descendantBlacklist": [] - } - } - }, - { - "icon": "slot", - "name": { - "zh_CN": "Slot" - }, - "component": "Slot", - "schema": { - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [ - { - "property": "name", - "type": "String", - "label": { - "text": { - "zh_CN": "插槽名称" - } - }, - "cols": 12, - "widget": { - "component": "InputConfigurator", - "props": {} - } - }, - { - "property": "params", - "type": "String", - "defaultValue": "", - "label": { - "text": { - "zh_CN": "作用域参数" - } - }, - "widget": { - "component": "CodeConfigurator", - "props": { - "language": "json", - "tips": { - "title": { "zh_CN": "提示:数据为数组类型" }, - "demo": { - "zh_CN": "示例:\n[\n {\n \"name\": \"text\",\n \"value\": {\n \"type\": \"JSExpression\",\n \"value\": \"this.state.greetingMessage\"\n }\n },\n {\n \"name\": \"count\",\n \"value\": 1\n }\n]" - } - } - } - } - } - ] - } - ], - "events": {}, - "shortcuts": { - "properties": [] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "isContainer": true - } - }, - { - "icon": "Collection", - "name": { - "zh_CN": "Collection" - }, - "component": "Collection", - "schema": { - "slots": {}, - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [ - { - "property": "condition", - "type": "Boolean", - "defaultValue": true, - "label": { - "text": { - "zh_CN": "是否渲染" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "SwitchConfigurator", - "props": {} - } - }, - { - "property": "style", - "type": "String", - "defaultValue": "", - "label": { - "text": { - "zh_CN": "样式" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "CodeConfigurator", - "props": {} - } - }, - { - "property": "dataSource", - "type": "String", - "defaultValue": "", - "bindState": false, - "label": { - "text": { - "zh_CN": "数据源" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "CollectionConfigurator", - "props": {} - } - } - ] - } - ], - "events": {}, - "shortcuts": { - "properties": [] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "isContainer": true - } - }, - { - "icon": "Text", - "name": { - "zh_CN": "Text" - }, - "component": "Text", - "schema": { - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [ - { - "property": "text", - "type": "String", - "defaultValue": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。", - "label": { - "text": { - "zh_CN": "文本内容" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "InputConfigurator", - "props": { - "type": "textarea", - "autosize": true - } - } - } - ] - } - ], - "events": { - "onClick": { - "label": { - "zh_CN": "点击事件" - }, - "description": { - "zh_CN": "点击时触发的回调函数" - }, - "type": "event", - "functionInfo": { - "params": [], - "returns": {} - }, - "defaultValue": "" - } - }, - "shortcuts": { - "properties": ["text"] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "loop": true - } - }, - { - "icon": "icon", - "name": { - "zh_CN": "Icon" - }, - "component": "Icon", - "container": false, - "schema": { - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [ - { - "property": "name", - "type": "String", - "defaultValue": "IconDel", - "bindState": true, - "label": { - "text": { - "zh_CN": "图标类型" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "SelectIconConfigurator", - "props": {} - } - } - ] - } - ], - "events": { - "onClick": { - "label": { - "zh_CN": "点击事件" - }, - "description": { - "zh_CN": "点击时触发的回调函数" - }, - "type": "event", - "functionInfo": { - "params": [], - "returns": {} - }, - "defaultValue": "" - } - }, - "shortcuts": { - "properties": ["name"] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "loop": true - } - }, - { - "icon": "Image", - "name": { - "zh_CN": "Img" - }, - "component": "Img", - "container": false, - "schema": { - "properties": [ - { - "label": { - "zh_CN": "基础信息" - }, - "description": { - "zh_CN": "基础信息" - }, - "collapse": { - "number": 6, - "text": { - "zh_CN": "显示更多" - } - }, - "content": [ - { - "property": "src", - "type": "String", - "defaultValue": "", - "bindState": true, - "label": { - "text": { - "zh_CN": "src路径" - } - }, - "cols": 12, - "rules": [], - "widget": { - "component": "InputConfigurator", - "props": {} - } - } - ] - } - ], - "events": { - "onClick": { - "label": { - "zh_CN": "点击事件" - }, - "description": { - "zh_CN": "点击时触发的回调函数" - }, - "type": "event", - "functionInfo": { - "params": [], - "returns": {} - }, - "defaultValue": "" - } - }, - "shortcuts": { - "properties": ["src"] - }, - "contentMenu": { - "actions": [] - } - }, - "configure": { - "loop": true - } - } - ], - "snippets": [ - { - "group": "layout", - "label": { - "zh_CN": "布局与容器" - }, - "children": [ - { - "name": { - "zh_CN": "盒子容器" - }, - "screenshot": "", - "snippetName": "Box", - "icon": "Box", - "schema": { - "componentName": "div", - "props": {} - } - } - ] - }, - { - "group": "basic", - "label": { - "zh_CN": "基础元素" - }, - "children": [ - { - "name": { - "zh_CN": "文本" - }, - "screenshot": "", - "snippetName": "Text", - "icon": "Text", - "schema": { - "componentName": "Text", - "props": { - "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。" - } - } - }, - { - "name": { - "zh_CN": "图标" - }, - "screenshot": "", - "snippetName": "Icon", - "icon": "icon", - "schema": { - "componentName": "Icon", - "props": { - "name": "IconDel" - } - } - }, - { - "name": { - "zh_CN": "图片" - }, - "screenshot": "", - "snippetName": "Img", - "icon": "Image", - "schema": { - "componentName": "Img", - "props": { - "src": "https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/designer-default-icon.jpg" - } - } - } - ] - }, - { - "group": "advanced", - "label": { - "zh_CN": "高级元素" - }, - "children": [ - { - "name": { - "zh_CN": "插槽" - }, - "screenshot": "", - "snippetName": "Slot", - "icon": "slot", - "schema": { - "componentName": "Slot", - "props": {} - } - }, - { - "name": { - "zh_CN": "数据源容器" - }, - "screenshot": "", - "snippetName": "Collection", - "icon": "Collection", - "schema": { - "componentName": "Collection", - "props": {} - } - } - ] - } - ] - } - } -} diff --git a/packages/canvas/render/src/builtin/helper.js b/packages/canvas/render/src/builtin/helper.js deleted file mode 100644 index 59200272e..000000000 --- a/packages/canvas/render/src/builtin/helper.js +++ /dev/null @@ -1,46 +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. - * - */ - -export const getStyleValue = (value) => { - if (typeof value === 'number' || /^\d+\.?\d*$/.test(value)) { - return `${value}px` - } - - if (/^\d+\.?\d*(px|%|pt|em|rem|vw|vh)$/.test(value)) { - return value - } - - return '' -} - -export const alignMap = { - 'flex-start': 'flex-start', - 'flex-end': 'flex-end', - center: 'center', - stretch: 'stretch', - start: 'start', - end: 'end' -} - -export const justAlignMap = { - 'space-between': 'space-between', - 'space-around': 'space-around', - 'space-evenly': 'space-evenly', - 'flex-start': 'flex-start', - 'flex-end': 'flex-end', - stretch: 'stretch', - center: 'center', - start: 'start', - end: 'end', - left: 'left', - right: 'right' -} diff --git a/packages/canvas/render/src/builtin/index.js b/packages/canvas/render/src/builtin/index.js deleted file mode 100644 index f857ff1a1..000000000 --- a/packages/canvas/render/src/builtin/index.js +++ /dev/null @@ -1,21 +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. - * - */ - -import CanvasText from './CanvasText.vue' -import CanvasBox from './CanvasBox.vue' -import CanvasCollection from './CanvasCollection.vue' -import CanvasIcon from './CanvasIcon.vue' -import CanvasSlot from './CanvasSlot.vue' -import CanvasImg from './CanvasImg.vue' -import CanvasPlaceholder from './CanvasPlaceholder.vue' - -export { CanvasText, CanvasBox, CanvasCollection, CanvasIcon, CanvasSlot, CanvasImg, CanvasPlaceholder } diff --git a/packages/canvas/render/src/context.js b/packages/canvas/render/src/context.js deleted file mode 100644 index 710cf3ed9..000000000 --- a/packages/canvas/render/src/context.js +++ /dev/null @@ -1,57 +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. - * - */ - -import { shallowReactive } from 'vue' -import { utils } from '@opentiny/tiny-engine-utils' - -export const context = shallowReactive({}) - -// 从大纲树控制隐藏 -export const conditions = shallowReactive({}) - -const nodes = {} - -export const setNode = (schema, parent) => { - schema.id = schema.id || utils.guid() - nodes[schema.id] = { node: schema, parent } -} - -export const getNode = (id, parent) => { - return parent ? nodes[id] : nodes[id].node -} - -export const delNode = (id) => delete nodes[id] - -export const clearNodes = () => { - Object.keys(nodes).forEach(delNode) -} - -export const getRoot = (id) => { - const { parent } = getNode(id, true) - - return parent?.id ? getRoot(parent.id) : parent -} - -export const setContext = (ctx, clear) => { - clear && Object.keys(context).forEach((key) => delete context[key]) - Object.assign(context, ctx) -} - -export const getContext = () => context - -export const setCondition = (id, visible = false) => { - conditions[id] = visible -} - -export const getCondition = (id) => conditions[id] !== false - -export const getConditions = () => conditions diff --git a/packages/canvas/render/src/lowcode.js b/packages/canvas/render/src/lowcode.js index 27214c2e7..da47785de 100644 --- a/packages/canvas/render/src/lowcode.js +++ b/packages/canvas/render/src/lowcode.js @@ -12,8 +12,9 @@ import { getCurrentInstance, nextTick, provide, inject } from 'vue' import { I18nInjectionKey } from 'vue-i18n' -import { api } from './RenderMain' -import { collectionMethodsMap, generateFn, globalNotify } from './render' +import { api } from '@opentiny/tiny-engine-renderer' + +const { getCollectionMethodsMap, generateFn, globalNotify } = api export const lowcodeWrap = (props, context) => { const global = {} @@ -61,7 +62,7 @@ export const lowcodeWrap = (props, context) => { const fnName = fn.name if (fn.toString().includes('return this')) { return () => global - } else if (fnName && collectionMethodsMap[fnName.slice(0, -1)]) { + } else if (fnName && getCollectionMethodsMap()[fnName.slice(0, -1)]) { // 这里区块打包的时候会在方法名称后面多加一个字符串,所以此处需要截取下函数名称 fn.realName = fnName.slice(0, -1) return generateFn(fn) diff --git a/packages/canvas/render/src/render.js b/packages/canvas/render/src/render.js deleted file mode 100644 index a27442061..000000000 --- a/packages/canvas/render/src/render.js +++ /dev/null @@ -1,729 +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. - * - */ - -import { h, provide, reactive } from 'vue' -import { isHTMLTag, hyphenate } from '@vue/shared' -import { useBroadcastChannel } from '@vueuse/core' -import { constants, utils } from '@opentiny/tiny-engine-utils' -import babelPluginJSX from '@vue/babel-plugin-jsx' -import { transformSync } from '@babel/core' -import i18nHost from '@opentiny/tiny-engine-i18n-host' -import { CanvasRow, CanvasCol, CanvasRowColContainer } from '@opentiny/tiny-engine-builtin-component' - -import { NODE_UID as DESIGN_UIDKEY, NODE_TAG as DESIGN_TAGKEY, NODE_LOOP as DESIGN_LOOPID } from '../../common' -import { context, conditions, setNode } from './context' -import { - CanvasBox, - CanvasCollection, - CanvasIcon, - CanvasText, - CanvasSlot, - CanvasImg, - CanvasPlaceholder -} from './builtin' - -const { BROADCAST_CHANNEL } = constants -const { hyphenateRE } = utils -const customElements = {} - -const transformJSX = (code) => { - const res = transformSync(code, { - plugins: [ - [ - babelPluginJSX, - { - pragma: 'h', - isCustomElement: (name) => customElements[name] - } - ] - ] - }) - return (res.code || '') - .replace(/import \{.+\} from "vue";/, '') - .replace(/h\(_?resolveComponent\((.*?)\)/g, `h(this.getComponent($1)`) - .replace(/_?resolveComponent/g, 'h') - .replace(/_?createTextVNode\((.*?)\)/g, '$1') - .trim() -} - -export const blockSlotDataMap = reactive({}) - -const Mapper = { - Icon: CanvasIcon, - Text: CanvasText, - Collection: CanvasCollection, - div: CanvasBox, - Slot: CanvasSlot, - slot: CanvasSlot, - Template: CanvasBox, - Img: CanvasImg, - CanvasRow, - CanvasCol, - CanvasRowColContainer, - CanvasPlaceholder -} - -const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify }) - -// 此处向外层window传递notify配置参数 -export const globalNotify = (options) => post(options) - -export const collectionMethodsMap = {} - -const getNative = (name) => { - return window.TinyLowcodeComponent?.[name] -} - -const getBlock = (name) => { - return window.blocks?.[name] -} - -const configure = {} -const controller = {} - -export const setConfigure = (configureData) => { - Object.assign(configure, configureData) -} - -export const setController = (controllerData) => { - Object.assign(controller, controllerData) -} - -export const getController = () => controller - -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 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 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 renderDefault = (children, scope, parent) => - children.map?.((child) => - // eslint-disable-next-line no-use-before-define - h(renderer, { - schema: child, - scope, - parent - }) - ) - -const parseJSSlot = (data, scope) => { - return ($scope) => renderDefault(data.value, { ...scope, ...$scope }, data) -} - -export const generateFn = (innerFn, context) => { - return (...args) => { - // 如果有数据源标识,则表格的fetchData返回数据源的静态数据 - const sourceId = 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) { - globalNotify({ - type: 'warning', - title: `函数:${innerFn.name}执行报错`, - message: error?.message || `函数:${innerFn.name}执行报错,请检查语法` - }) - } - - // 这里注意如果innerFn返回的是一个promise则需要捕获异常,重新返回默认一条空数据 - if (result.then) { - result = new Promise((resolve) => { - result.then(resolve).catch((error) => { - globalNotify({ - type: 'warning', - title: '异步函数执行报错', - message: error?.message || '异步函数执行报错,请检查语法' - }) - // 这里需要至少返回一条空数据,方便用户使用表格默认插槽 - resolve({ - result: [{}], - page: { total: 1 } - }) - }) - }) - } - - return result - } - } -} - -// 解析函数字符串结构 -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 -} - -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 generateBlockContent = (schema) => { - if (schema?.componentName === 'Collection') { - generateCollection(schema) - } - if (Array.isArray(schema?.children)) { - schema.children.forEach((item) => { - generateBlockContent(item) - }) - } -} - -const registerBlock = (componentName) => { - getController() - .registerBlock?.(componentName) - .then((res) => { - const blockSchema = res.content - - // 拿到区块数据,建立区块中数据源的映射关系 - generateBlockContent(blockSchema) - - // 如果区块的根节点有百分比高度,则需要特殊处理,把高度百分比传递下去,适配大屏应用 - if (/height:\s*?[\d|.]+?%/.test(blockSchema?.props?.style)) { - const blockDoms = document.querySelectorAll(hyphenate(componentName)) - blockDoms.forEach((item) => { - item.style.height = '100%' - }) - } - }) -} - -export const wrapCustomElement = (componentName) => { - const material = getController().getMaterial(componentName) - - if (!Object.keys(material).length) { - registerBlock(componentName) - } - - customElements[componentName] = { - name: componentName + '.ce', - render() { - return h( - hyphenate(componentName), - window.parent.TinyGlobalConfig.dslMode === 'Vue' ? getPlainProps(this.$attrs) : this.$attrs, - this.$slots.default?.() - ) - } - } - - return customElements[componentName] -} - -export const getComponent = (name) => { - return ( - Mapper[name] || - getNative(name) || - getBlock(name) || - customElements[name] || - (isHTMLTag(name) ? name : wrapCustomElement(name)) - ) -} - -// 解析JSX字符串为可执行函数 -const parseJSXFunction = (data, 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 - }) - } catch (error) { - globalNotify({ - type: 'warning', - title: '函数声明解析报错', - message: error?.message || '函数声明解析报错,请检查语法' - }) - - return newFn() - } -} - -const parseJSFunction = (data, scope, ctx = context) => { - try { - const innerFn = newFn(`return ${data.value}`).bind(ctx)() - return generateFn(innerFn, ctx) - } catch (error) { - return parseJSXFunction(data, ctx) - } -} - -const parseList = [] - -export function parseData(data, scope, ctx = context) { - let res = data - parseList.some((item) => { - if (item.type(data)) { - res = item.parseFunc(data, scope, ctx) - - return true - } - - return false - }) - - return res -} - -const parseCondition = (condition, scope, ctx = context) => { - // eslint-disable-next-line no-eq-null - return condition == null ? true : parseData(condition, scope, ctx) -} - -const parseLoopArgs = (_loop) => { - if (_loop) { - const { item, index, loopArgs = '' } = _loop - const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }` - return newFn('item,index', body)(item, index) - } - return undefined -} - -export const getIcon = (name) => window.TinyVueIcon?.[name]?.() || '' - -const parseObjectData = (data, scope, ctx) => { - if (!data) { - return data - } - - // 如果是状态访问器,则直接解析默认值 - if (isStateAccessor(data)) { - return parseData(data.defaultValue) - } - - // 解析通过属性传递icon图标组件 - if (data.componentName === 'Icon') { - return getIcon(data.props.name) - } - const res = {} - Object.entries(data).forEach(([key, value]) => { - // 如果是插槽则需要进行特殊处理 - if (key === 'slot' && value?.name) { - res[key] = value.name - } else { - res[key] = parseData(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: isString, - parseFunc: parseString - }, - { - type: isArray, - parseFunc: parseArray - }, - { - type: isFunction, - parseFunc: parseFunction - }, - { - type: isObject, - parseFunc: parseObjectData - } - ] -) - -const stopEvent = (event) => { - event.preventDefault?.() - event.stopPropagation?.() - return false -} - -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 - - 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) => { - 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 }, currentSlot.parent) - }) - - return slots - } - - return { default: () => renderDefault(children, scope, schema) } -} - -const checkGroup = (componentName) => configure[componentName]?.nestingRule?.childWhitelist?.length - -const clickCapture = (componentName) => configure[componentName]?.clickCapture !== false - -const getBindProps = (schema, scope) => { - const { id, componentName } = schema - const invalidity = configure[componentName]?.invalidity || [] - - if (componentName === 'CanvasPlaceholder') { - return {} - } - - const bindProps = { - ...parseData(schema.props, scope), - [DESIGN_UIDKEY]: id, - [DESIGN_TAGKEY]: componentName, - onMouseover: stopEvent, - onFocus: stopEvent - } - if (scope) { - bindProps[DESIGN_LOOPID] = scope.index === undefined ? scope.idx : scope.index - } - - // 在捕获阶段阻止事件的传播 - if (clickCapture(componentName)) { - bindProps.onClickCapture = stopEvent - } - - if (Mapper[componentName]) { - bindProps.schema = schema - } - - // 绑定组件属性时需要将 className 重命名为 class,防止覆盖组件内置 class - bindProps.class = bindProps.className - delete bindProps.className - - // 使画布中元素可拖拽 - bindProps.draggable = true - - // 过滤在门户网站上配置的画布丢弃的属性 - invalidity.forEach((prop) => delete bindProps[prop]) - - 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, parent) => { - return children.map?.((schema) => { - const { componentName, children, loop, loopArgs, condition, id } = schema - const loopList = parseData(loop, scope) - - const renderElement = (item, index) => { - const mergeScope = getLoopScope({ - scope, - index, - item, - loopArgs - }) - - setNode(schema, parent) - - if (conditions[id] === false || !parseCondition(condition, mergeScope)) { - return null - } - - const renderChildren = injectPlaceHolder(componentName, children) - - return h( - getComponent(componentName), - getBindProps(schema, mergeScope), - Array.isArray(renderChildren) - ? renderSlot(renderChildren, mergeScope, schema) - : parseData(renderChildren, mergeScope) - ) - } - - return loopList?.length ? loopList.map(renderElement) : renderElement() - }) -} - -const getChildren = (schema, mergeScope) => { - const { componentName, children } = schema - const renderChildren = injectPlaceHolder(componentName, children) - - const component = getComponent(componentName) - const isNative = typeof component === 'string' - const isCustomElm = customElements[componentName] - const isGroup = checkGroup(componentName) - - if (Array.isArray(renderChildren)) { - if (isNative || isCustomElm) { - return renderDefault(renderChildren, mergeScope, schema) - } else { - return isGroup - ? renderGroup(renderChildren, mergeScope, schema) - : renderSlot(renderChildren, mergeScope, schema, isCustomElm) - } - } else { - return parseData(renderChildren, mergeScope) - } -} - -export const renderer = { - name: 'renderer', - props: { - schema: Object, - scope: Object, - parent: Object - }, - setup(props) { - provide('schema', props.schema) - }, - render() { - const { scope, schema, parent } = this - const { componentName, loop, loopArgs, condition } = schema - - // 处理数据源和表格fetchData的映射关系 - generateCollection(schema) - - if (!componentName) { - return parseData(schema, scope) - } - - const component = getComponent(componentName) - - const loopList = parseData(loop, scope) - - const renderElement = (item, index) => { - let mergeScope = item - ? getLoopScope({ - item, - index, - loopArgs, - scope - }) - : scope - - // 如果是区块,并且使用了区块的作用域插槽,则需要将作用域插槽的数据传递下去 - if (parent?.componentType === 'Block' && componentName === 'Template' && schema.props?.slot?.params?.length) { - const slotName = schema.props.slot?.name || schema.props.slot - const blockName = parent.componentName - const slotData = blockSlotDataMap[blockName]?.[slotName] || {} - mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData - } - - // 给每个节点设置schema.id,并缓存起来 - setNode(schema, parent) - - if (conditions[schema.id] === false || !parseCondition(condition, mergeScope)) { - return null - } - - return h(component, getBindProps(schema, mergeScope), getChildren(schema, mergeScope)) - } - - return loopList?.length ? loopList.map(renderElement) : renderElement() - } -} - -export default renderer diff --git a/packages/canvas/render/src/runner.js b/packages/canvas/render/src/runner.js index deb1ce1b9..dea812146 100644 --- a/packages/canvas/render/src/runner.js +++ b/packages/canvas/render/src/runner.js @@ -13,9 +13,10 @@ import { createApp } from 'vue' import { addScript, addStyle, dynamicImportComponents, updateDependencies } from '../../common' import TinyI18nHost, { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n' -import Main, { api } from './RenderMain' +import Main, { api } from '@opentiny/tiny-engine-renderer' import lowcode from './lowcode' import { supportUmdBlock } from './supportUmdBlock' +import CanvasEmpty from './CanvasEmpty.vue' const dispatch = (name, data) => { window.parent.document.dispatchEvent(new CustomEvent(name, data)) @@ -62,6 +63,8 @@ const create = async (config) => { dispatch('canvasReady', { detail: renderer }) + api.setCanvasEmptyComponent(CanvasEmpty) + App = createApp(Main).use(TinyI18nHost).provide(I18nInjectionKey, TinyI18nHost) if (typeof appCreated === 'function') { diff --git a/packages/canvas/render/src/supportUmdBlock.js b/packages/canvas/render/src/supportUmdBlock.js index b07d50658..60c8a3dcf 100644 --- a/packages/canvas/render/src/supportUmdBlock.js +++ b/packages/canvas/render/src/supportUmdBlock.js @@ -5,7 +5,9 @@ import * as TinyVueIcon from '@opentiny/vue-icon' import TinyVue from '@opentiny/vue' import TinyI18nHost from '@opentiny/tiny-engine-common/js/i18n' import { camelize, capitalize } from '@vue/shared' -import { blockSlotDataMap, getComponent } from './render' +import { api } from '@opentiny/tiny-engine-renderer' + +const { getBlockSlotDataMap, getComponent } = api // 和 @opentiny/tiny-engine-block-build 打包umd方式相适配 export function supportUmdBlock() { @@ -33,6 +35,8 @@ export function supportUmdBlock() { // 如果是作用域插槽,则获取作用域插槽传递过来的参数 if (slotData) { + const blockSlotDataMap = getBlockSlotDataMap() + if (blockSlotDataMap[blockName]) { blockSlotDataMap[blockName][slotName] = slotData } else { diff --git a/packages/renderer/src/RenderMain.js b/packages/renderer/src/RenderMain.js index c057e0f8c..9bfa85a89 100644 --- a/packages/renderer/src/RenderMain.js +++ b/packages/renderer/src/RenderMain.js @@ -37,9 +37,10 @@ import { getCondition, getConditions, context, - setNode + setNode, + getCanvasFlag, + setCanvasFlag } from './context' -import CanvasEmpty from './CanvasEmpty.vue' const { BROADCAST_CHANNEL } = constants const { parseFunction: generateFunction } = commonUtils @@ -366,6 +367,14 @@ const setSchema = async (data) => { const getNode = (id, parent) => (id ? getNodeById(id, parent) : schema) +let CanvasEmptyComponent = null + +const getCanvasEmptyComponent = () => (CanvasEmptyComponent ? h(CanvasEmptyComponent) : null) + +const setCanvasEmptyComponent = (component) => { + CanvasEmptyComponent = component +} + let canvasRenderer = null const defaultRenderer = function () { @@ -385,7 +394,7 @@ const defaultRenderer = function () { ref: 'page', className: 'design-page' }, - schema.children?.length ? h(renderer, { schema: rootChildrenSchema, parent: schema }) : [h(CanvasEmpty)] + schema.children?.length ? h(renderer, { schema: rootChildrenSchema, parent: schema }) : getCanvasEmptyComponent() ) } @@ -466,5 +475,8 @@ export const api = { generateFn, getCollectionMethodsMap, getBlockSlotDataMap, - getComponent + getComponent, + getCanvasFlag, + setCanvasFlag, + setCanvasEmptyComponent } diff --git a/packages/renderer/src/builtin/CanvasCollection.js b/packages/renderer/src/builtin/CanvasCollection.js index b5cb22762..40c671bfd 100644 --- a/packages/renderer/src/builtin/CanvasCollection.js +++ b/packages/renderer/src/builtin/CanvasCollection.js @@ -10,8 +10,7 @@ * */ -import { getController } from '../render' -import { api } from '../RenderMain' +import { api } from '@opentiny/tiny-engine-renderer' import { useModal } from '@opentiny/tiny-engine-meta-register' const NAME_PREFIX = { @@ -45,7 +44,7 @@ const genRemoteMethodToLifeSetup = (variableName, sourceRef, pageSchema) => { const removeState = (pageSchema, variableName) => { delete pageSchema.state[variableName] - const { parse, traverse, generate } = getController().ast + const { parse, traverse, generate } = api.getController().ast const setupFn = pageSchema.lifeCycles?.setup?.value try { @@ -76,7 +75,7 @@ const defaultHandlerTemplate = ({ node, sourceRef, schemaId, pageSchema }) => { const genVarName = (schemaId) => `${NAME_PREFIX.loop}${schemaId}` const updateNode = () => { - const { configure } = getController().getMaterial(node?.componentName) + const { configure } = api.getController().getMaterial(node?.componentName) if (!configure?.loop) { return diff --git a/packages/renderer/src/builtin/CanvasCollection.vue b/packages/renderer/src/builtin/CanvasCollection.vue index d033924f3..7a85b46fe 100644 --- a/packages/renderer/src/builtin/CanvasCollection.vue +++ b/packages/renderer/src/builtin/CanvasCollection.vue @@ -8,13 +8,13 @@