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