diff --git a/package.json b/package.json
index af2d9e7d9..69f335f60 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"devDependencies": {
"@babel/eslint-parser": "^7.21.3",
"@types/node": "^18.0.0",
+ "@vue/eslint-config-typescript": "^11.0.3",
"chokidar": "^3.5.3",
"concurrently": "^8.2.0",
"cross-env": "^7.0.3",
diff --git a/packages/build/vite-config/src/default-config.js b/packages/build/vite-config/src/default-config.js
index 54f0b86b5..e19a1e778 100644
--- a/packages/build/vite-config/src/default-config.js
+++ b/packages/build/vite-config/src/default-config.js
@@ -29,7 +29,7 @@ const getDefaultConfig = (engineConfig) => {
base: './',
publicDir: path.resolve(root, './public'),
resolve: {
- extensions: ['.js', '.jsx', '.vue'],
+ extensions: ['.js', '.jsx', '.vue', '.ts', '.tsx'],
alias: {}
},
server: {
diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
index 2f2cd3b58..f9610e3d9 100644
--- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
+++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
@@ -53,7 +53,7 @@ const getDevAlias = (useSourceAlias) => {
'@opentiny/tiny-engine-theme-light': path.resolve(basePath, 'packages/theme/light/index.less'),
'@opentiny/tiny-engine-theme-base': path.resolve(basePath, 'packages/theme/base/src/index.js'),
'@opentiny/tiny-engine-svgs': path.resolve(basePath, 'packages/svgs/index.js'),
- '@opentiny/tiny-engine-canvas/render': path.resolve(basePath, 'packages/canvas/render/index.js'),
+ '@opentiny/tiny-engine-canvas/render': path.resolve(basePath, 'packages/canvas/render/index.ts'),
'@opentiny/tiny-engine-canvas': path.resolve(basePath, 'packages/canvas/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'),
diff --git a/packages/canvas/.eslintrc.cjs b/packages/canvas/.eslintrc.cjs
index 7e042cff2..6dec99b0a 100644
--- a/packages/canvas/.eslintrc.cjs
+++ b/packages/canvas/.eslintrc.cjs
@@ -9,7 +9,6 @@
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
-
module.exports = {
env: {
browser: true,
@@ -17,16 +16,16 @@ module.exports = {
node: true,
jest: true
},
- extends: ['eslint:recommended', 'plugin:vue/vue3-essential'],
+ extends: ['eslint:recommended', 'plugin:vue/vue3-essential', '@vue/eslint-config-typescript'],
parser: 'vue-eslint-parser',
parserOptions: {
- parser: '@babel/eslint-parser',
+ parser: '@typescript-eslint/parser',
ecmaVersion: 'latest',
sourceType: 'module',
requireConfigFile: false,
babelOptions: {
parserOpts: {
- plugins: ['jsx']
+ plugins: ['jsx', 'typescript']
}
}
},
@@ -37,6 +36,8 @@ module.exports = {
'space-before-function-paren': 'off',
'vue/multi-word-component-names': 'off',
'no-use-before-define': 'error',
- 'no-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }]
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
+ 'import/no-inner-modules': 'off'
}
}
diff --git a/packages/canvas/DesignCanvas/src/DesignCanvas.vue b/packages/canvas/DesignCanvas/src/DesignCanvas.vue
index 2633d88ce..0247818ff 100644
--- a/packages/canvas/DesignCanvas/src/DesignCanvas.vue
+++ b/packages/canvas/DesignCanvas/src/DesignCanvas.vue
@@ -18,7 +18,7 @@
diff --git a/packages/canvas/render/src/builtin/CanvasRouterView.vue b/packages/canvas/render/src/builtin/CanvasRouterView.vue
index 96beea7c2..ad0b3b728 100644
--- a/packages/canvas/render/src/builtin/CanvasRouterView.vue
+++ b/packages/canvas/render/src/builtin/CanvasRouterView.vue
@@ -1,11 +1,6 @@
-
-
-
-
-
+
-
diff --git a/packages/canvas/render/src/canvas-function/canvas-api.ts b/packages/canvas/render/src/canvas-function/canvas-api.ts
new file mode 100644
index 000000000..ca31e1b68
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/canvas-api.ts
@@ -0,0 +1,80 @@
+import type { useBridge, useDataSourceMap, useGlobalState, useUtils } from '../application-function'
+import type { IPageContext, useSchema } from '../page-block-function'
+import type { useCustomRenderer } from './custom-renderer'
+import type { setConfigure } from '../material-function'
+import type { getDesignMode, setDesignMode } from './design-mode'
+import type { setController } from './controller'
+
+export interface IApplicationFunctionAPI
+ extends Pick, 'getUtils' | 'setUtils' | 'updateUtils' | 'deleteUtils'>,
+ Pick, 'getBridge' | 'setBridge'>,
+ Pick, 'getGlobalState' | 'setGlobalState'>,
+ Pick, 'getDataSourceMap' | 'setDataSourceMap'> {}
+export interface IPageFunctionAPI
+ extends Pick<
+ ReturnType,
+ | 'getSchema'
+ | 'setSchema'
+ | 'setState'
+ | 'deleteState'
+ | 'getState'
+ | 'getProps'
+ | 'setProps'
+ | 'getMethods'
+ | 'setMethods'
+ | 'setPagecss'
+ > {}
+export interface IPageContextAPI
+ extends Pick<
+ IPageContext,
+ 'getContext' | 'getNode' | 'getRoot' | 'setNode' | 'setCondition' | 'getCondition' | 'getConditions'
+ > {}
+export interface ICanvasFunctionAPI extends Pick, 'getRenderer' | 'setRenderer'> {
+ getDesignMode: typeof getDesignMode
+ setDesignMode: typeof setDesignMode
+ setController: typeof setController
+ setConfigure: typeof setConfigure
+}
+export type IInnerCanvasAPI = IApplicationFunctionAPI & IPageFunctionAPI & ICanvasFunctionAPI & IPageContextAPI
+
+let currentApi: IInnerCanvasAPI
+
+export function setCurrentApi(activeApi) {
+ currentApi = activeApi
+}
+
+export const api: IInnerCanvasAPI = {
+ getUtils: (...args) => currentApi?.getUtils(...args),
+ setUtils: (...args) => currentApi?.setUtils(...args),
+ updateUtils: (...args) => currentApi?.updateUtils(...args),
+ deleteUtils: (...args) => currentApi?.deleteUtils(...args),
+ getBridge: (...args) => currentApi?.getBridge(...args),
+ setBridge: (...args) => currentApi?.setBridge(...args),
+ getMethods: (...args) => currentApi?.getMethods(...args),
+ setMethods: (...args) => currentApi?.setMethods(...args),
+ setController: (...args) => currentApi?.setController(...args),
+ setConfigure: (...args) => currentApi?.setConfigure(...args),
+ getSchema: (...args) => currentApi?.getSchema(...args),
+ setSchema: (...args) => currentApi?.setSchema(...args),
+ getState: (...args) => currentApi?.getState(...args),
+ deleteState: (...args) => currentApi?.deleteState(...args),
+ setState: (...args) => currentApi?.setState(...args),
+ getProps: (...args) => currentApi?.getProps(...args),
+ setProps: (...args) => currentApi?.setProps(...args),
+ getContext: (...args) => currentApi?.getContext(...args),
+ getNode: (...args) => currentApi?.getNode(...args),
+ getRoot: (...args) => currentApi?.getRoot(...args),
+ setPagecss: (...args) => currentApi?.setPagecss(...args),
+ setCondition: (...args) => currentApi?.setCondition(...args),
+ getCondition: (...args) => currentApi?.getCondition(...args),
+ getConditions: (...args) => currentApi?.getConditions(...args),
+ getGlobalState: (...args) => currentApi?.getGlobalState(...args),
+ getDataSourceMap: (...args) => currentApi?.getDataSourceMap(...args),
+ setDataSourceMap: (...args) => currentApi?.setDataSourceMap(...args),
+ setGlobalState: (...args) => currentApi?.setGlobalState(...args),
+ setNode: (...args) => currentApi?.setNode(...args),
+ getRenderer: (...args) => currentApi?.getRenderer(...args),
+ setRenderer: (...args) => currentApi?.setRenderer(...args),
+ getDesignMode: (...args) => currentApi?.getDesignMode(...args),
+ setDesignMode: (...args) => currentApi?.setDesignMode(...args)
+}
diff --git a/packages/canvas/render/src/canvas-function/controller.ts b/packages/canvas/render/src/canvas-function/controller.ts
new file mode 100644
index 000000000..166caeac8
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/controller.ts
@@ -0,0 +1,7 @@
+const controller: Record = {}
+
+export const setController = (controllerData) => {
+ Object.assign(controller, controllerData)
+}
+
+export const getController = () => controller
diff --git a/packages/canvas/render/src/canvas-function/custom-renderer.ts b/packages/canvas/render/src/canvas-function/custom-renderer.ts
new file mode 100644
index 000000000..8b7ba8edf
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/custom-renderer.ts
@@ -0,0 +1,54 @@
+import { h } from 'vue'
+import CanvasEmpty from './CanvasEmpty.vue'
+import renderer from '../render'
+
+function defaultRenderer(schema, refreshKey, entry, active, isPage = true) {
+ // 渲染画布增加根节点,与出码和预览保持一致
+ const rootChildrenSchema = {
+ componentName: 'div',
+ // 手动添加一个唯一的属性,后续在画布选中此节点时方便处理额外的逻辑。由于没有修改schema,不会影响出码
+ props: { ...schema.props, 'data-id': 'root-container', 'data-page-active': active },
+ children: schema.children
+ }
+
+ if (!entry) {
+ return schema.children?.length || !active
+ ? h(renderer, { schema: rootChildrenSchema, parent: schema })
+ : [h(CanvasEmpty)]
+ }
+
+ const PageStartSchema = {
+ componentName: 'div',
+ componentType: 'PageStart',
+ props: { 'data-id': 'root-container' }
+ }
+
+ return h(
+ 'tiny-i18n-host',
+ {
+ locale: 'zh_CN',
+ key: refreshKey.value,
+ ref: 'page',
+ className: 'design-page'
+ },
+ isPage
+ ? h(renderer, { schema: PageStartSchema, parent: schema })
+ : schema.children?.length
+ ? h(renderer, { schema: rootChildrenSchema, parent: schema })
+ : [h(CanvasEmpty)]
+ )
+}
+
+export function useCustomRenderer() {
+ let canvasRenderer = null
+
+ const getRenderer = () => canvasRenderer || defaultRenderer
+ const setRenderer = (fn) => {
+ canvasRenderer = fn
+ }
+
+ return {
+ getRenderer,
+ setRenderer
+ }
+}
diff --git a/packages/canvas/render/src/canvas-function/design-mode.ts b/packages/canvas/render/src/canvas-function/design-mode.ts
new file mode 100644
index 000000000..977bc05e7
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/design-mode.ts
@@ -0,0 +1,13 @@
+export const DESIGN_MODE = {
+ DESIGN: 'design', // 设计态
+ RUNTIME: 'runtime' // 运行态
+}
+
+// 是否表现画布内特征的标志,用来控制是否允许拖拽、原生事件是否触发等
+let designMode = DESIGN_MODE.DESIGN
+
+export const getDesignMode = () => designMode
+
+export const setDesignMode = (mode) => {
+ designMode = mode
+}
diff --git a/packages/canvas/render/src/canvas-function/global-notify.ts b/packages/canvas/render/src/canvas-function/global-notify.ts
new file mode 100644
index 000000000..189902901
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/global-notify.ts
@@ -0,0 +1,7 @@
+import { useBroadcastChannel } from '@vueuse/core'
+import { constants } from '@opentiny/tiny-engine-utils'
+const { BROADCAST_CHANNEL } = constants
+const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.Notify })
+
+// 此处向外层window传递notify配置参数
+export const globalNotify = (options) => post(options)
diff --git a/packages/canvas/render/src/canvas-function/index.ts b/packages/canvas/render/src/canvas-function/index.ts
new file mode 100644
index 000000000..2c5e3d4f3
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/index.ts
@@ -0,0 +1,5 @@
+export * from './controller'
+export * from './design-mode'
+export * from './global-notify'
+export * from './custom-renderer'
+export * from './locale'
diff --git a/packages/canvas/render/src/canvas-function/locale.ts b/packages/canvas/render/src/canvas-function/locale.ts
new file mode 100644
index 000000000..9973baea9
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/locale.ts
@@ -0,0 +1,13 @@
+import { inject, watch, WritableComputedRef } from 'vue'
+import { I18nInjectionKey } from 'vue-i18n'
+import { useBroadcastChannel } from '@vueuse/core'
+import { constants } from '@opentiny/tiny-engine-utils'
+
+const { BROADCAST_CHANNEL } = constants
+export function useLocale() {
+ const { locale } = inject(I18nInjectionKey).global
+ const { data } = useBroadcastChannel({ name: BROADCAST_CHANNEL.CanvasLang })
+ watch(data, () => {
+ ;(locale as WritableComputedRef).value = data.value
+ })
+}
diff --git a/packages/canvas/render/src/canvas-function/page-switcher.ts b/packages/canvas/render/src/canvas-function/page-switcher.ts
new file mode 100644
index 000000000..5b8489457
--- /dev/null
+++ b/packages/canvas/render/src/canvas-function/page-switcher.ts
@@ -0,0 +1,19 @@
+import { reactive } from 'vue'
+import { IPageContext } from '../page-block-function'
+
+export interface ICurrentPage {
+ pageId: string | number
+ schema: any
+ pageContext: IPageContext
+}
+export const currentPage = reactive({
+ pageId: null,
+ schema: null,
+ pageContext: null
+})
+
+export function setCurrentPage({ pageId, schema, pageContext }: ICurrentPage) {
+ currentPage.pageId = pageId
+ currentPage.pageContext = pageContext
+ currentPage.schema = schema
+}
diff --git a/packages/canvas/render/src/context.js b/packages/canvas/render/src/context.js
deleted file mode 100644
index 67bbf6080..000000000
--- a/packages/canvas/render/src/context.js
+++ /dev/null
@@ -1,71 +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
-
-export const DESIGN_MODE = {
- DESIGN: 'design', // 设计态
- RUNTIME: 'runtime' // 运行态
-}
-
-// 是否表现画布内特征的标志,用来控制是否允许拖拽、原生事件是否触发等
-let designMode = DESIGN_MODE.DESIGN
-
-export const getDesignMode = () => designMode
-
-export const setDesignMode = (mode) => {
- designMode = mode
-}
diff --git a/packages/canvas/render/src/data-function/index.ts b/packages/canvas/render/src/data-function/index.ts
new file mode 100644
index 000000000..e9312eb33
--- /dev/null
+++ b/packages/canvas/render/src/data-function/index.ts
@@ -0,0 +1 @@
+export * from './parser'
diff --git a/packages/canvas/render/src/data-function/parser.ts b/packages/canvas/render/src/data-function/parser.ts
new file mode 100644
index 000000000..081b33d9a
--- /dev/null
+++ b/packages/canvas/render/src/data-function/parser.ts
@@ -0,0 +1,310 @@
+import babelPluginJSX from '@vue/babel-plugin-jsx'
+import { transformSync } from '@babel/core'
+import i18nHost from '@opentiny/tiny-engine-i18n-host'
+
+import { globalNotify } from '../canvas-function'
+import { collectionMethodsMap, customElements, getComponent, getIcon } from '../material-function'
+import { newFn } from '../data-utils'
+import { renderDefault } from '../render'
+
+interface ITypeParserDef {
+ type: (data) => boolean
+ parseFunc: (data: unknown, scope: Record, ctx: Record) => unknown
+}
+
+const parseList: Array = []
+
+const isI18nData = (data) => {
+ return data && data.type === 'i18n'
+}
+
+const isJSSlot = (data) => {
+ return data && data.type === 'JSSlot'
+}
+
+const isJSExpression = (data) => {
+ return data && data.type === 'JSExpression'
+}
+
+const isJSFunction = (data) => {
+ return data && data.type === 'JSFunction'
+}
+
+const isJSResource = (data) => {
+ return data && data.type === 'JSResource'
+}
+
+const isString = (data) => {
+ return typeof data === 'string'
+}
+
+const isArray = (data) => {
+ return Array.isArray(data)
+}
+
+const isFunction = (data) => {
+ return typeof data === 'function'
+}
+const isIcon = (data) => {
+ return data?.componentName === 'Icon'
+}
+const isObject = (data) => {
+ return typeof data === 'object'
+}
+// 判断是否是状态访问器
+export const isStateAccessor = (stateData) =>
+ stateData?.accessor?.getter?.type === 'JSFunction' || stateData?.accessor?.setter?.type === 'JSFunction'
+
+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()
+}
+
+const parseExpression = (data, scope, ctx, isJsx = false) => {
+ try {
+ if (data.value.indexOf('this.i18n') > -1) {
+ ctx.i18n = i18nHost.global.t
+ } else if (data.value.indexOf('t(') > -1) {
+ ctx.t = i18nHost.global.t
+ }
+
+ const expression = isJsx ? transformJSX(data.value) : data.value
+ return newFn('$scope', `with($scope || {}) { return ${expression} }`).call(ctx, {
+ ...ctx,
+ ...scope,
+ slotScope: scope
+ })
+ } catch (err) {
+ // 解析抛出异常,则再尝试解析 JSX 语法。如果解析 JSX 语法仍然出现错误,isJsx 变量会确保不会再次递归执行解析
+ if (!isJsx) {
+ return parseExpression(data, scope, ctx, true)
+ }
+ return undefined
+ }
+}
+
+const parseI18n = (i18n, scope, ctx) => {
+ return parseExpression(
+ {
+ type: 'JSExpression',
+ value: `this.i18n('${i18n.key}', ${JSON.stringify(i18n.params)})`
+ },
+ scope,
+ { i18n: i18nHost.global.t, ...ctx }
+ )
+}
+
+// 解析函数字符串结构
+const parseFunctionString = (fnStr) => {
+ const fnRegexp = /(async)?.*?(\w+) *\(([\s\S]*?)\) *\{([\s\S]*)\}/
+ const result = fnRegexp.exec(fnStr)
+ if (result) {
+ return {
+ type: result[1] || '',
+ name: result[2],
+ params: result[3]
+ .split(',')
+ .map((item) => item.trim())
+ .filter((item) => Boolean(item)),
+ body: result[4]
+ }
+ }
+ return null
+}
+
+// 解析JSX字符串为可执行函数
+const parseJSXFunction = (data, _scope, ctx) => {
+ try {
+ const newValue = transformJSX(data.value)
+ const fnInfo = parseFunctionString(newValue)
+ if (!fnInfo) throw Error('函数解析失败,请检查格式。示例:function fnName() { }')
+
+ return newFn(...fnInfo.params, fnInfo.body).bind({
+ ...ctx,
+ getComponent
+ })
+ } catch (error) {
+ globalNotify({
+ type: 'warning',
+ title: '函数声明解析报错',
+ message: error?.message || '函数声明解析报错,请检查语法'
+ })
+
+ return newFn()
+ }
+}
+
+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 parseJSFunction = (data, _scope, ctx) => {
+ try {
+ const innerFn = newFn(`return ${data.value}`).bind(ctx)()
+ return generateFn(innerFn, ctx)
+ } catch (error) {
+ return parseJSXFunction(data, null, ctx)
+ }
+}
+
+const parseJSSlot = (data, scope, _ctx) => {
+ return ($scope) => renderDefault(data.value, { ...scope, ...$scope }, data)
+}
+
+export function parseData(data, scope, ctx) {
+ const typeParser = parseList.find((item) => item.type(data))
+ return typeParser ? typeParser.parseFunc(data, scope, ctx) : data
+}
+
+export const parseCondition = (condition, scope, ctx) => {
+ // eslint-disable-next-line no-eq-null
+ return condition == null ? true : parseData(condition, scope, ctx)
+}
+
+export const parseLoopArgs = (loop?: { item: unknown; index: number; loopArgs?: string[] }) => {
+ if (!loop) {
+ return undefined
+ }
+ const { item, index, loopArgs = [] } = loop
+ const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }`
+ return newFn('item,index', body)(item, index)
+}
+
+const parseIcon = (data, _scope, _ctx) => {
+ return getIcon(data.props.name)
+}
+
+const parseStateAccessor = (data, _scope, ctx) => {
+ return parseData(data.defaultValue, null, ctx)
+}
+
+const parseObjectData = (data, scope, ctx) => {
+ if (!data) {
+ return data
+ }
+
+ const res = {}
+ Object.entries(data).forEach(([key, value]: [string, any]) => {
+ // 如果是插槽则需要进行特殊处理
+ if (key === 'slot' && value?.name) {
+ res[key] = value.name
+ } else {
+ res[key] = parseData(value, scope, ctx)
+ }
+ })
+ return res
+}
+
+const parseString = (data) => {
+ return data.trim()
+}
+
+const parseArray = (data, scope, ctx) => {
+ return data.map((item) => parseData(item, scope, ctx))
+}
+
+const parseFunction = (data, scope, ctx) => {
+ return data.bind(ctx)
+}
+
+parseList.push(
+ ...[
+ {
+ type: isJSExpression,
+ parseFunc: parseExpression
+ },
+ {
+ type: isI18nData,
+ parseFunc: parseI18n
+ },
+ {
+ type: isJSFunction,
+ parseFunc: parseJSFunction
+ },
+ {
+ type: isJSResource,
+ parseFunc: parseExpression
+ },
+ {
+ type: isJSSlot,
+ parseFunc: parseJSSlot
+ },
+ {
+ type: isIcon,
+ parseFunc: parseIcon
+ },
+ {
+ type: isStateAccessor,
+ parseFunc: parseStateAccessor
+ },
+ {
+ type: isString,
+ parseFunc: parseString
+ },
+ {
+ type: isArray,
+ parseFunc: parseArray
+ },
+ {
+ type: isFunction,
+ parseFunc: parseFunction
+ },
+ {
+ type: isObject,
+ parseFunc: parseObjectData
+ }
+ ]
+)
diff --git a/packages/canvas/render/src/data-utils.ts b/packages/canvas/render/src/data-utils.ts
new file mode 100644
index 000000000..867256da4
--- /dev/null
+++ b/packages/canvas/render/src/data-utils.ts
@@ -0,0 +1,12 @@
+import { utils as commonUtils } from '@opentiny/tiny-engine-utils'
+export const { parseFunction: generateFunction } = commonUtils
+
+export const reset = (obj) => {
+ Object.keys(obj).forEach((key) => delete obj[key])
+}
+
+// 规避创建function eslint报错
+export const newFn = (...argv) => {
+ const Fn = Function
+ return new Fn(...argv)
+}
diff --git a/packages/canvas/render/src/lowcode.js b/packages/canvas/render/src/lowcode.ts
similarity index 92%
rename from packages/canvas/render/src/lowcode.js
rename to packages/canvas/render/src/lowcode.ts
index 27214c2e7..c3b577903 100644
--- a/packages/canvas/render/src/lowcode.js
+++ b/packages/canvas/render/src/lowcode.ts
@@ -13,14 +13,16 @@
import { getCurrentInstance, nextTick, provide, inject } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'
import { api } from './RenderMain'
-import { collectionMethodsMap, generateFn, globalNotify } from './render'
+import { globalNotify } from './canvas-function'
+import { collectionMethodsMap } from './material-function'
+import { generateFn } from './data-function'
export const lowcodeWrap = (props, context) => {
- const global = {}
+ const global: Record = {}
const instance = getCurrentInstance()
const router = ''
const route = ''
- const { t, locale } = inject(I18nInjectionKey).global
+ const { t, locale } = inject(I18nInjectionKey).global as any
const emit = context.emit
const ref = (ref) => instance.refs[ref]
diff --git a/packages/canvas/render/src/material-function/configure.ts b/packages/canvas/render/src/material-function/configure.ts
new file mode 100644
index 000000000..918b9dc6d
--- /dev/null
+++ b/packages/canvas/render/src/material-function/configure.ts
@@ -0,0 +1,4 @@
+export const configure: Record = {}
+export const setConfigure = (configureData) => {
+ Object.assign(configure, configureData)
+}
diff --git a/packages/canvas/render/src/material-function/handle-scoped-css.ts b/packages/canvas/render/src/material-function/handle-scoped-css.ts
new file mode 100644
index 000000000..0f259507c
--- /dev/null
+++ b/packages/canvas/render/src/material-function/handle-scoped-css.ts
@@ -0,0 +1,6 @@
+import postcss from 'postcss'
+import scopedPlugin from './scope-css-plugin'
+
+export function handleScopedCss(id: string, content: string) {
+ return postcss([scopedPlugin(id)]).process(content)
+}
diff --git a/packages/canvas/render/src/material-function/index.ts b/packages/canvas/render/src/material-function/index.ts
new file mode 100644
index 000000000..80b7f888a
--- /dev/null
+++ b/packages/canvas/render/src/material-function/index.ts
@@ -0,0 +1,4 @@
+export * from './configure'
+export * from './material-getter'
+export * from './support-collection'
+export * from './support-block-slot-data-for-webcomponent'
diff --git a/packages/canvas/render/src/material-function/material-getter.ts b/packages/canvas/render/src/material-function/material-getter.ts
new file mode 100644
index 000000000..be9da1e82
--- /dev/null
+++ b/packages/canvas/render/src/material-function/material-getter.ts
@@ -0,0 +1,132 @@
+import { h } from 'vue'
+import { isHTMLTag, hyphenate } from '@vue/shared'
+import * as TinyVueIcon from '@opentiny/vue-icon'
+import { utils } from '@opentiny/tiny-engine-utils'
+import { CanvasRow, CanvasCol, CanvasRowColContainer } from '@opentiny/tiny-engine-builtin-component'
+import {
+ CanvasBox,
+ CanvasCollection,
+ CanvasIcon,
+ CanvasText,
+ CanvasSlot,
+ CanvasImg,
+ CanvasPlaceholder,
+ CanvasRouterView,
+ CanvasRouterLink
+} from '../builtin'
+import { getController } from '../canvas-function/controller'
+import { generateCollection } from './support-collection'
+
+export const customElements = {}
+export const Mapper = {
+ Icon: CanvasIcon,
+ Text: CanvasText,
+ Collection: CanvasCollection,
+ div: CanvasBox,
+ Slot: CanvasSlot,
+ slot: CanvasSlot,
+ Template: CanvasBox,
+ Img: CanvasImg,
+ CanvasRow,
+ CanvasCol,
+ CanvasRowColContainer,
+ CanvasPlaceholder,
+ RouterView: CanvasRouterView,
+ RouterLink: CanvasRouterLink
+}
+const getNative = (name) => {
+ return window.TinyLowcodeComponent?.[name]
+}
+
+const getBlock = (name) => {
+ return window.blocks?.[name]
+}
+
+const { hyphenateRE } = utils
+const getPlainProps = (object: Record = {}) => {
+ 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 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 getIcon = (name) => TinyVueIcon?.[name]?.() || ''
+
+export const getComponent = (name) => {
+ return (
+ Mapper[name] ||
+ getNative(name) ||
+ getBlock(name) ||
+ customElements[name] ||
+ (isHTMLTag(name) ? name : wrapCustomElement(name))
+ )
+}
diff --git a/packages/canvas/render/src/material-function/page-getter.ts b/packages/canvas/render/src/material-function/page-getter.ts
new file mode 100644
index 000000000..fe6d5b67e
--- /dev/null
+++ b/packages/canvas/render/src/material-function/page-getter.ts
@@ -0,0 +1,90 @@
+import { defineComponent, h, onUnmounted, ref, watch } from 'vue'
+import { getController } from '../canvas-function'
+import RenderMain from '../RenderMain'
+import { handleScopedCss } from './handle-scoped-css'
+
+const pageSchema: Record = {}
+
+async function fetchPageSchema(pageId: string) {
+ return getController()
+ .getPageById(pageId)
+ .then((res) => {
+ return res.page_content
+ })
+}
+const styleSheetMap = new Map()
+export function initStyle(key: string, content: string) {
+ if (styleSheetMap.get(key) || !content) {
+ return
+ }
+ const styleSheet = new CSSStyleSheet()
+ styleSheetMap.set(key, styleSheet)
+ document.adoptedStyleSheets.push(styleSheet)
+ handleScopedCss(key, content).then((scopedCss) => {
+ styleSheet.replaceSync(scopedCss)
+ })
+}
+export const wrapPageComponent = (pageId: string) => {
+ const key = `data-te-page-${pageId}`
+ const asyncData = ref(null)
+ const updateSchema = () => {
+ fetchPageSchema(pageId).then((data) => {
+ asyncData.value = data
+ initStyle(key, data?.css)
+ })
+ }
+ updateSchema() // 保证加载一份非编辑态schema,减少页面跳转渲染时间
+ pageSchema[pageId] = defineComponent({
+ name: `page-${pageId}`,
+ setup() {
+ const active = ref(pageId === getController().getBaseInfo().pageId)
+ const stop = getController().addHistoryDataChangedCallback(() => {
+ const newValue = pageId === getController().getBaseInfo().pageId
+ if (active.value !== newValue) {
+ active.value = newValue
+ }
+ })
+ const watchStop = watch(
+ () => active.value,
+ (activeValue) => {
+ if (!activeValue) {
+ updateSchema()
+ }
+ }
+ )
+ onUnmounted(() => {
+ stop()
+ watchStop()
+ })
+
+ return () => {
+ if (active.value || asyncData.value) {
+ h(RenderMain, {
+ cssScopeId: key,
+ renderSchema: asyncData.value,
+ active: active.value,
+ pageId: pageId,
+ entry: false
+ })
+ }
+ return null
+ }
+ }
+ })
+ return pageSchema[pageId]
+}
+export const getPage = (pageId: string) => {
+ return pageSchema[pageId] || wrapPageComponent(pageId)
+}
+
+export async function getPageAncestors(pageId?: string) {
+ if (!pageId) {
+ return []
+ }
+ if (!getController().getPageAncestors) {
+ // 如果不支持查询祖先 则返回自己
+ return [pageId]
+ }
+ const pageChain = await getController().getPageAncestors(pageId)
+ return [...pageChain.map((id: number | string) => String(id)), pageId]
+}
diff --git a/packages/canvas/render/src/material-function/scope-css-plugin.ts b/packages/canvas/render/src/material-function/scope-css-plugin.ts
new file mode 100644
index 000000000..8f56f6535
--- /dev/null
+++ b/packages/canvas/render/src/material-function/scope-css-plugin.ts
@@ -0,0 +1,193 @@
+/** @ref {@vue/compiler-sfc@2.7.16/src/stylePlugins/scoped.ts } */
+/* eslint-disable no-use-before-define, prefer-const*/
+import { PluginCreator, Rule, AtRule } from 'postcss'
+import selectorParser from 'postcss-selector-parser'
+
+const animationNameRE = /^(-\w+-)?animation-name$/
+const animationRE = /^(-\w+-)?animation$/
+
+const scopedPlugin: PluginCreator = (id = '') => {
+ const keyframes = Object.create(null)
+ const shortId = id.replace(/^data-v-/, '')
+
+ return {
+ postcssPlugin: 'vue-sfc-scoped',
+ Rule(rule) {
+ processRule(id, rule)
+ },
+ AtRule(node) {
+ if (/-?keyframes$/.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
+ // register keyframes
+ keyframes[node.params] = node.params = node.params + '-' + shortId
+ }
+ },
+ OnceExit(root) {
+ if (Object.keys(keyframes).length) {
+ // If keyframes are found in this