diff --git a/.github/workflows/push-check.yml b/.github/workflows/push-check.yml index 0acaab2bdd..03d23d21c5 100644 --- a/.github/workflows/push-check.yml +++ b/.github/workflows/push-check.yml @@ -4,40 +4,40 @@ on: push: branches: [] pull_request: - branches: [develop,main, refactor/develop] + branches: [develop, main, refactor/develop, release/*] jobs: push-check: - runs-on: ubuntu-latest # windows-latest || macos-latest - + runs-on: ubuntu-latest # windows-latest || macos-latest + steps: - - uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - uses: actions/setup-node@v4 - with: - node-version: 18 + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: 18 - - name: Install dependencies - run: pnpm i + - name: Install dependencies + run: pnpm i - - name: Get changed files - id: get_changed_files - uses: tj-actions/changed-files@v41 - with: - files: | - **.js - **.vue - **.jsx - - name: Run ESLint - run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}} - - name: Run Build - run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1 + - name: Get changed files + id: get_changed_files + uses: tj-actions/changed-files@v41 + with: + files: | + **.js + **.vue + **.jsx + - name: Run ESLint + run: npx eslint ${{steps.get_changed_files.outputs.all_changed_files}} + - name: Run Build + run: pnpm run build:plugin && pnpm run build:alpha > build-alpha.log 2>&1 - - name: Upload build logs - uses: actions/upload-artifact@v4 - with: - name: build-alpha-log - path: build-alpha.log + - name: Upload build logs + uses: actions/upload-artifact@v4 + with: + name: build-alpha-log + path: build-alpha.log diff --git a/designer-demo/package.json b/designer-demo/package.json index 30822897f7..f39d5ae222 100644 --- a/designer-demo/package.json +++ b/designer-demo/package.json @@ -1,7 +1,7 @@ { "name": "designer-demo", "private": true, - "version": "2.0.0-rc.4", + "version": "2.1.0", "type": "module", "scripts": { "dev": "cross-env VITE_THEME=light vite", diff --git a/designer-demo/public/mock/bundle.json b/designer-demo/public/mock/bundle.json index 358e84215b..328a4f2775 100644 --- a/designer-demo/public/mock/bundle.json +++ b/designer-demo/public/mock/bundle.json @@ -44,25 +44,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "type", - "size" - ] + "properties": ["type", "size"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -336,25 +324,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "type", - "size" - ] + "properties": ["type", "size"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -660,9 +636,7 @@ "isModal": false, "isPopper": false, "nestingRule": { - "childWhitelist": [ - "ElFormItem" - ], + "childWhitelist": ["ElFormItem"], "parentWhitelist": "", "descendantBlacklist": "", "ancestorWhitelist": "" @@ -671,25 +645,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "inline", - "label-width" - ] + "properties": ["inline", "label-width"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -1138,25 +1100,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "inline", - "label-width" - ] + "properties": ["inline", "label-width"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -1490,9 +1440,7 @@ "isModal": false, "isPopper": false, "nestingRule": { - "childWhitelist": [ - "ElTableColumn" - ], + "childWhitelist": ["ElTableColumn"], "parentWhitelist": "", "descendantBlacklist": "", "ancestorWhitelist": "" @@ -1501,25 +1449,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "inline", - "label-width" - ] + "properties": ["inline", "label-width"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -2750,25 +2686,13 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "inline", - "label-width" - ] + "properties": ["inline", "label-width"] }, "contextMenu": { - "actions": [ - "copy", - "remove", - "insert", - "updateAttr", - "bindEevent", - "createBlock" - ], + "actions": ["copy", "remove", "insert", "updateAttr", "bindEevent", "createBlock"], "disable": [] }, - "invalidity": [ - "" - ], + "invalidity": [""], "clickCapture": true, "framework": "Vue" }, @@ -2914,19 +2838,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "size" - ] + "properties": ["disabled", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -3243,9 +3159,7 @@ "clickCapture": false, "isModal": false, "nestingRule": { - "childWhitelist": [ - "TinyCarouselItem" - ], + "childWhitelist": ["TinyCarouselItem"], "parentWhitelist": "", "descendantBlacklist": "", "ancestorWhitelist": "" @@ -3254,19 +3168,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "size" - ] + "properties": ["disabled", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -3418,14 +3324,7 @@ "name": { "zh_CN": "标题" }, - "component": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "h6" - ], + "component": ["h1", "h2", "h3", "h4", "h5", "h6"], "icon": "h16", "description": "标题", "docUrl": "", @@ -3510,19 +3409,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "size" - ] + "properties": ["disabled", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -3879,19 +3770,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "size" - ] + "properties": ["disabled", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -4137,9 +4020,7 @@ ], "events": {}, "shortcuts": { - "properties": [ - "src" - ] + "properties": ["src"] }, "contentMenu": { "actions": [] @@ -4789,19 +4670,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "size" - ] + "properties": ["disabled", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -4947,19 +4820,118 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label-width", - "disabled" - ] + "properties": ["label-width", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] + } + } + }, + { + "icon": "row", + "name": { + "zh_CN": "row" + }, + "component": "TinyLayout", + "description": "定义 Layout 的行配置信息", + "docUrl": "", + "screenshot": "", + "tags": "", + "keywords": "", + "devMode": "proCode", + "npm": { + "package": "@opentiny/vue", + "exportName": "Layout", + "version": "3.14.0", + "destructuring": true, + "script": "https://unpkg.com/@opentiny/vue@~3.14/runtime/tiny-vue.mjs", + "css": "https://unpkg.com/@opentiny/vue-theme@~3.14/index.css" + }, + "group": "component", + "priority": 5, + "schema": { + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "content": [ + { + "property": "cols", + "label": { + "text": { + "zh_CN": "总栅格数" + } + }, + "cols": 12, + "widget": { + "component": "ButtonGroupConfigurator", + "props": { + "options": [ + { + "label": "12", + "value": 12 + }, + { + "label": "24", + "value": 24 + } + ] + } + }, + "description": { + "zh_CN": "选择总栅格数" + }, + "labelPosition": "none" + }, + { + "property": "tag", + "label": { + "text": { + "zh_CN": "layout渲染的标签" + } + }, + "required": false, + "readOnly": false, + "disabled": false, + "cols": 12, + "widget": { + "component": "InputConfigurator", + "props": {} + }, + "description": { + "zh_CN": "定义Layout元素渲染后的标签,默认为 div" + } + } + ] + } + ] + }, + "configure": { + "loop": true, + "condition": true, + "styles": true, + "isContainer": true, + "isModal": false, + "nestingRule": { + "childWhitelist": ["TinyRow", "TinyCol"], + "parentWhitelist": "", + "descendantBlacklist": "", + "ancestorWhitelist": "" + }, + "isNullNode": false, + "isLayout": false, + "rootSelector": "", + "shortcuts": { + "properties": ["disabled"] + }, + "contextMenu": { + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -5309,19 +5281,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label-width", - "disabled" - ] + "properties": ["label-width", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -5441,9 +5405,7 @@ "isModal": false, "nestingRule": { "childWhitelist": "", - "parentWhitelist": [ - "TinyForm" - ], + "parentWhitelist": ["TinyForm"], "descendantBlacklist": "", "ancestorWhitelist": "" }, @@ -5451,19 +5413,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label", - "rules" - ] + "properties": ["label", "rules"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -5737,19 +5691,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label", - "rules" - ] + "properties": ["label", "rules"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -6084,19 +6030,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "text", - "size" - ] + "properties": ["text", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -6506,19 +6444,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "value", - "disabled" - ] + "properties": ["value", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -6751,19 +6681,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "visible", - "width" - ] + "properties": ["visible", "width"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -7164,19 +7086,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "multiple", - "options" - ] + "properties": ["multiple", "options"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -7371,19 +7285,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "mini" - ] + "properties": ["disabled", "mini"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -7654,19 +7560,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "clearable", - "mini" - ] + "properties": ["clearable", "mini"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -7921,19 +7819,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "border", - "disabled" - ] + "properties": ["border", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -8117,19 +8007,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "text", - "size" - ] + "properties": ["text", "size"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -8334,19 +8216,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "type" - ] + "properties": ["disabled", "type"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -8620,19 +8494,11 @@ "isLayout": false, "rootSelector": ".tiny-dialog-box", "shortcuts": { - "properties": [ - "visible", - "width" - ] + "properties": ["visible", "width"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -8900,9 +8766,7 @@ "clickCapture": false, "isModal": false, "nestingRule": { - "childWhitelist": [ - "TinyTabItem" - ], + "childWhitelist": ["TinyTabItem"], "parentWhitelist": [], "descendantBlacklist": [], "ancestorWhitelist": [] @@ -8911,19 +8775,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "size", - "tab-style" - ] + "properties": ["size", "tab-style"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9020,9 +8876,7 @@ "isModal": false, "nestingRule": { "childWhitelist": "", - "parentWhitelist": [ - "TinyTab" - ], + "parentWhitelist": ["TinyTab"], "descendantBlacklist": "", "ancestorWhitelist": "" }, @@ -9030,19 +8884,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "name", - "title" - ] + "properties": ["name", "title"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9168,9 +9014,7 @@ "clickCapture": false, "isModal": false, "nestingRule": { - "childWhitelist": [ - "TinyBreadcrumbItem" - ], + "childWhitelist": ["TinyBreadcrumbItem"], "parentWhitelist": [], "descendantBlacklist": [], "ancestorWhitelist": [] @@ -9179,18 +9023,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "separator" - ] + "properties": ["separator"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9267,9 +9104,7 @@ "isModal": false, "nestingRule": { "childWhitelist": "", - "parentWhitelist": [ - "TinyBreadcrumb" - ], + "parentWhitelist": ["TinyBreadcrumb"], "descendantBlacklist": "", "ancestorWhitelist": "" }, @@ -9277,18 +9112,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "to" - ] + "properties": ["to"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9411,19 +9239,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label-width", - "disabled" - ] + "properties": ["label-width", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9530,19 +9350,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "label-width", - "disabled" - ] + "properties": ["label-width", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -9757,10 +9569,7 @@ "widget": { "component": "JsSlotConfigurator", "props": { - "slots": [ - "header", - "default" - ] + "slots": ["header", "default"] } } }, @@ -9823,8 +9632,7 @@ }, "description": { "zh_CN": "单元格编辑渲染配置项,也可以是函数 Function(h, params)" - }, - "labelPosition": "left" + } }, { "property": "filter", @@ -9912,7 +9720,7 @@ "required": true, "readOnly": false, "disabled": false, - "onChange": "this.delProp('data')", + "onChange": "function () { this.delProp('data') } ", "cols": 12, "widget": { "component": "CodeConfigurator", @@ -10433,15 +10241,10 @@ } }, "shortcuts": { - "properties": [ - "sortable", - "columns" - ] + "properties": ["sortable", "columns"] }, "contentMenu": { - "actions": [ - "create symbol" - ] + "actions": ["create symbol"] }, "onBeforeMount": "console.log('table on load'); this.pager = source.pager; this.fetchData = source.fetchData; this.data = source.data ;this.columns = source.columns" }, @@ -10461,19 +10264,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "sortable", - "columns" - ] + "properties": ["sortable", "columns"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -10702,19 +10497,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "currentPage", - "total" - ] + "properties": ["currentPage", "total"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -11073,19 +10860,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "modelValue", - "disabled" - ] + "properties": ["modelValue", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -11416,19 +11195,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "data", - "show-checkbox" - ] + "properties": ["data", "show-checkbox"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -11631,19 +11402,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "active", - "data" - ] + "properties": ["active", "data"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -11860,19 +11623,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "disabled", - "content" - ] + "properties": ["disabled", "content"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -12363,19 +12118,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "visible", - "width" - ] + "properties": ["visible", "width"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -12786,19 +12533,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "value", - "disabled" - ] + "properties": ["value", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } }, @@ -13237,19 +12976,11 @@ "isLayout": false, "rootSelector": "", "shortcuts": { - "properties": [ - "value", - "disabled" - ] + "properties": ["value", "disabled"] }, "contextMenu": { - "actions": [ - "create symbol" - ], - "disable": [ - "copy", - "remove" - ] + "actions": ["create symbol"], + "disable": ["copy", "remove"] } } } @@ -13634,10 +13365,7 @@ "schema": { "componentName": "TinyCheckboxGroup", "props": { - "modelValue": [ - "name1", - "name2" - ], + "modelValue": ["name1", "name2"], "type": "checkbox", "options": [ { @@ -14462,4 +14190,4 @@ ] } } -} \ No newline at end of file +} diff --git a/mockServer/package.json b/mockServer/package.json index 10c1e7c4f2..dfac7ce031 100644 --- a/mockServer/package.json +++ b/mockServer/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-mock", - "version": "2.0.0-rc.4", + "version": "2.1.0", "publishConfig": { "access": "public" }, diff --git a/packages/block-compiler/.eslintrc.cjs b/packages/block-compiler/.eslintrc.cjs new file mode 100644 index 0000000000..eb1014c689 --- /dev/null +++ b/packages/block-compiler/.eslintrc.cjs @@ -0,0 +1,27 @@ +const path = require('path') +const { rules } = require('../../.eslintrc') + +module.exports = { + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + projectService: true, + project: [path.join(__dirname, './tsconfig.json') ], + ecmaVersion: 'latest', + }, + plugins: ['@typescript-eslint'], + env: { + browser: true, + es2015: true, + node: true + }, + rules: { + ...rules, + // 允许 @ts-ignore + "@typescript-eslint/ban-ts-comment": "off", + // 允许非空断言 + "@typescript-eslint/no-non-null-asserted-optional-chain": "off" + }, + ignorePatterns: ['test/sample/*.vue', '.eslintrc.cjs'] +} diff --git a/packages/block-compiler/README.md b/packages/block-compiler/README.md new file mode 100644 index 0000000000..c840a7126b --- /dev/null +++ b/packages/block-compiler/README.md @@ -0,0 +1,2 @@ +# @opentiny/tiny-engine 低代码引擎区编译器 + diff --git a/packages/block-compiler/index.html b/packages/block-compiler/index.html new file mode 100644 index 0000000000..f0dde4b8cc --- /dev/null +++ b/packages/block-compiler/index.html @@ -0,0 +1,35 @@ + + + + + + block-compiler + + + + +
+ + + diff --git a/packages/block-compiler/package.json b/packages/block-compiler/package.json new file mode 100644 index 0000000000..15496257b6 --- /dev/null +++ b/packages/block-compiler/package.json @@ -0,0 +1,45 @@ +{ + "name": "@opentiny/tiny-engine-block-compiler", + "version": "2.1.0", + "publishConfig": { + "access": "public" + }, + "description": "block runtime compiler", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/opentiny/tiny-engine", + "directory": "packages/block-compiler" + }, + "bugs": { + "url": "https://github.com/opentiny/tiny-engine/issues" + }, + "author": "OpenTiny Team", + "license": "MIT", + "homepage": "https://opentiny.design/tiny-engine", + "devDependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@esbuild-plugins/node-modules-polyfill": "^0.2.2", + "@types/babel__core": "^7.20.5", + "@vitejs/plugin-vue": "^5.1.2", + "typescript": "~5.4.2", + "vite": "^5.4.2", + "vite-plugin-dts": "^4.3.0" + }, + "peerDependencies": { + "@babel/core": "^7.26.0", + "@vue/babel-plugin-jsx": "^1.2.5", + "@vue/compiler-sfc": "^3.4.15", + "vue": "^3.4.15" + } +} diff --git a/packages/block-compiler/src/dev.ts b/packages/block-compiler/src/dev.ts new file mode 100644 index 0000000000..1c6344e228 --- /dev/null +++ b/packages/block-compiler/src/dev.ts @@ -0,0 +1,78 @@ +// @ts-ignore +import { createApp, defineAsyncComponent, h } from 'https://unpkg.com/vue@3.4.23/dist/vue.runtime.esm-browser.js' +import { compile } from './index' +import BlockFileName from '../test/sample/BlockFileName.vue?raw' +import BlockHead from '../test/sample/BlockHead.vue?raw' +import BlockMenu from '../test/sample/BlockMenu.vue?raw' +import BlockTest from '../test/sample/BlockTest.vue?raw' +import BlockJsxTest from '../test/sample/slotModelValueTest.vue?raw' + +const RenderMain = { + setup() { + const componentMap = compile( + [ + { + fileName: 'BlockHead', + sourceCode: BlockHead + }, + { + fileName: 'BlockFileName', + sourceCode: BlockFileName + }, + { + fileName: 'BlockMenu', + sourceCode: BlockMenu + }, + { + fileName: 'BlockTest', + sourceCode: BlockTest + }, + { + fileName: 'BlockJsxTest', + sourceCode: BlockJsxTest + } + ], + {} + ) + + const blockComponents: { [key: string]: unknown } = {} + + // @ts-ignore + window.getBlockComponentBlobUrl = (name) => { + return componentMap?.[name]?.blobURL + } + + for (const [fileName, value] of Object.entries(componentMap)) { + blockComponents[fileName] = defineAsyncComponent(() => import(/* @vite-ignore */ value.blobURL)) + } + + const css = Object.values(componentMap) + .map((item) => item.style) + .join('') + + const stylesheet = document.querySelector('#block-stylesheet') + + if (stylesheet) { + stylesheet.remove() + } else { + const newStyleSheet = document.createElement('style') + + newStyleSheet.innerHTML = css + + document.head.appendChild(newStyleSheet) + } + + return () => + h('div', {}, [ + h(blockComponents.BlockJsxTest), + h(blockComponents.BlockTest), + h(blockComponents.BlockHead), + h(blockComponents.BlockFileName), + h('span', {}, 'testtest') + ]) + } +} + +const App = createApp(RenderMain) + +App.mount(document.querySelector('#app')!) diff --git a/packages/block-compiler/src/index.ts b/packages/block-compiler/src/index.ts new file mode 100644 index 0000000000..6f2c340d8c --- /dev/null +++ b/packages/block-compiler/src/index.ts @@ -0,0 +1,308 @@ +import { compileScript, compileTemplate, compileStyle, parse, babelParse, MagicString } from '@vue/compiler-sfc' +import type { SFCParseResult, SFCDescriptor, BindingMetadata, CompilerOptions } from '@vue/compiler-sfc' +import { testIsJsx, transformVueJsx } from './transformJsx.ts' + +const compileBlockStyle = (descriptor: SFCDescriptor, id: string) => { + const cssResArr = descriptor.styles.map((style) => { + const result = compileStyle({ + id, + filename: descriptor.filename, + source: style.content, + scoped: style.scoped + }) + + return result.code || '' + }) + + return cssResArr.join('\n') +} + +const compileBlockTemplate = (descriptor: SFCDescriptor, id: string, bindingMetadata: BindingMetadata | undefined) => { + const isJsx = testIsJsx(descriptor) + const expressionPlugins: CompilerOptions['expressionPlugins'] = [] + + if (isJsx) { + expressionPlugins.push('jsx') + } + + const compileRes = compileTemplate({ + id, + ast: descriptor.template?.ast, + source: descriptor.template?.content!, + filename: descriptor.filename, + scoped: descriptor.styles.some((styleItem) => styleItem.scoped), + slotted: descriptor.slotted, + compilerOptions: { + bindingMetadata, + expressionPlugins + } + }) + + const { errors } = compileRes + let { code } = compileRes + + if (isJsx) { + code = transformVueJsx(code) || '' + } + + return { code, errors } +} + +interface compiledItem { + js: string + style: string + blobURL: string +} + +export interface IResultMap { + [key: string]: compiledItem +} + +const resolveRelativeImport = (code: string, globalGetterName = 'loadBlockComponent') => { + const magicStr = new MagicString(code) + const ast = babelParse(code, { sourceType: 'module', plugins: ['jsx'] }).program.body + + let vueImportNode = null + let hasDefineAsyncComponent = false + + for (const node of ast) { + if (node.type === 'ImportDeclaration') { + const source = node.source.value + if (source === 'vue' && !node.specifiers.find((spec) => spec.type === 'ImportNamespaceSpecifier')) { + vueImportNode = node + } + // 标识相对路径引入的 .vue 文件为区块,使用异步组件替换 + if (source.startsWith('./') && node.source.value.endsWith('.vue')) { + hasDefineAsyncComponent = true + const fileName = node.source.value.replace(/^(\.\/+)/, '').slice(0, -4) + // 默认导出名 + const defaultImportId = node.specifiers.find((spec) => spec.type === 'ImportDefaultSpecifier')?.local?.name + + // 不存在默认导出,跳过 + if (!defaultImportId) { + continue + } + + // 声明异步组件 const Block = defineAsyncComponent(() => import(getBlockUrl(Block))) + magicStr.appendLeft( + node.start!, + `const ${defaultImportId} = defineAsyncComponent(() => window.${globalGetterName}('${fileName}'))` + ) + + // 移除 import Block from './Block.vue' 语句 + magicStr.remove(node.start!, node.end!) + } + } + } + + // TODO: 拿到类型声明,拆分到另一个函数 + if (hasDefineAsyncComponent) { + // 存在异步引入组件 + if (vueImportNode) { + const asyncSpec = vueImportNode.specifiers.find( + (spec) => spec.type === 'ImportSpecifier' && spec.local.name === 'defineAsyncComponent' + ) + + if (!asyncSpec) { + const firstRelativeSpec = vueImportNode.specifiers.find((spec) => spec.type === 'ImportSpecifier') + + if (firstRelativeSpec) { + magicStr.appendLeft(firstRelativeSpec.start!, 'defineAsyncComponent, ') + } else { + magicStr.appendRight(vueImportNode.specifiers[0].end!, ', { defineAsyncComponent }') + } + } + } else { + magicStr.appendLeft(ast[0].start!, "import { defineAsyncComponent } from 'vue'\n") + } + } + + return magicStr.toString() +} + +const DEFAULT_COMPONENT_NAME = '__sfc__' + +const compileBlockScript = (descriptor: SFCDescriptor, id: string): [string, BindingMetadata | undefined] => { + const isJsx = testIsJsx(descriptor) + const expressionPlugins: CompilerOptions['expressionPlugins'] = [] + + if (isJsx) { + expressionPlugins.push('jsx') + } + + // TODO: try catch + const compiledScript = compileScript(descriptor, { + genDefaultAs: DEFAULT_COMPONENT_NAME, + inlineTemplate: true, + id, + templateOptions: { + compilerOptions: { + expressionPlugins + } + } + }) + + let code = compiledScript.content + + if (isJsx) { + code = transformVueJsx(code) || '' + } + + return [code, compiledScript.bindings] +} + +interface IParsedFileItem { + fileName: string + sourceCode: string + compilerParseResult: SFCParseResult + importedFiles: string[] + fileNameWithRelativePath: string +} + +// 依次构建 script、template、style,然后组装成 import +const compileFile = (file: IParsedFileItem): Omit => { + const descriptor = file.compilerParseResult.descriptor + + // 编译 script + const [compiledScript, bindings] = compileBlockScript(descriptor, file.fileName) + let componentCode = `${compiledScript}` + + // 编译 template + if (!descriptor.scriptSetup && descriptor.template) { + const { code: compiledTemplate } = compileBlockTemplate(descriptor, file.fileName, bindings) + + componentCode += `\n ${compiledTemplate} \n ${DEFAULT_COMPONENT_NAME}.render = render` + } + + const hasScoped = descriptor.styles.some((styleItem) => styleItem.scoped) + + if (hasScoped) { + componentCode += `\n${DEFAULT_COMPONENT_NAME}.__scopeId='data-v-${file.fileName}'` + } + + // 编译 style + const styleString = compileBlockStyle(descriptor, file.fileName) + + return { + js: `${componentCode}\nexport default ${DEFAULT_COMPONENT_NAME}`, + style: styleString + } +} + +// 解析依赖的文件 +const parseImportedFiles = (descriptor: SFCDescriptor): string[] => { + let scriptContent = '' + + if (descriptor.script) { + scriptContent = descriptor.script.content + } else if (descriptor.scriptSetup) { + scriptContent = descriptor.scriptSetup.content + } + + if (!scriptContent) { + return [] + } + + const ast = babelParse(scriptContent, { sourceFilename: descriptor.filename, sourceType: 'module', plugins: ['jsx'] }) + .program.body + const res: string[] = [] + + for (const node of ast) { + if (node.type === 'ImportDeclaration') { + const source = node.source.value + + // 相对路径依赖,区块嵌套的场景 + if (source.startsWith('./')) { + res.push(node.source.value) + } + } + } + + return res +} + +const getJSBlobURL = (str: string) => { + const blob = new Blob([str], { type: 'application/javascript' }) + + return URL.createObjectURL(blob) +} + +export interface IFileItem { + fileName: string + sourceCode: string +} + +export type IFileList = IFileItem[] + +export interface IConfig { + compileCache?: Map + globalGetterName?: string +} + +// TODO: 支持 importMap +export const compile = (fileList: IFileList, config: IConfig) => { + const parsedFileList = fileList.map((fileItem) => { + const { fileName, sourceCode } = fileItem + // FIXME:这里解析的结果不能重复使用,因为可能会涉及修改引入的依赖 + const { descriptor, errors } = parse(sourceCode, { filename: fileName }) + + if (errors) { + // TODO: 抛出错误 + } + + // TODO: 1. 当前仅支持 vue 文件编译,检查文件后缀,如果不是 .vue 结尾的,抛出错误 + // TODO: 2. 检查 style lang,仅支持 css + // TODO: 3. 检查 template lang,当前不支持任何 template lang + + // 解析依赖的文件 + const importedFiles = parseImportedFiles(descriptor) + + return { + fileName, + sourceCode, + compilerParseResult: { + descriptor, + errors + }, + importedFiles, + fileNameWithRelativePath: `./${fileName}.vue` + } + }) + + const compiledFilesSet: Set = new Set() + const resultMap: IResultMap = {} + + const compileCache = config?.compileCache || new Map() + + for (const fileItem of parsedFileList) { + const fileName = fileItem.fileName + const cache = compileCache.get(fileName) + let js = '' + let style = '' + + // 优先使用缓存 + if (cache?.js && cache?.style) { + js = cache.js + style = cache.style + } else { + const compileRes = compileFile(fileItem) + + js = compileRes.js + style = compileRes.style + } + + const resolvedImportJs = resolveRelativeImport(js, config?.globalGetterName) + + resultMap[fileName] = { + js: resolvedImportJs, + style, + blobURL: getJSBlobURL(resolvedImportJs) + } + + compileCache.set(fileName, resultMap[fileName]) + + compiledFilesSet.add(fileItem.fileNameWithRelativePath) + } + + return resultMap +} diff --git a/packages/block-compiler/src/transformJsx.ts b/packages/block-compiler/src/transformJsx.ts new file mode 100644 index 0000000000..900ade2a73 --- /dev/null +++ b/packages/block-compiler/src/transformJsx.ts @@ -0,0 +1,18 @@ +import { transformSync } from '@babel/core' +import vueJsx from '@vue/babel-plugin-jsx' +import type { SFCDescriptor } from 'vue/compiler-sfc' + +export const testIsJsx = (descriptor: SFCDescriptor) => { + const lang = descriptor.script?.lang || descriptor?.scriptSetup?.lang || '' + + return /jsx$/.test(lang) +} + +export const transformVueJsx = (sourceCode: string) => { + return transformSync(sourceCode, { + babelrc: false, + plugins: [vueJsx], + sourceMaps: false, + configFile: false + })?.code +} diff --git a/packages/block-compiler/src/vite-env.d.ts b/packages/block-compiler/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/block-compiler/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/block-compiler/test/sample/BlockFileName.vue b/packages/block-compiler/test/sample/BlockFileName.vue new file mode 100644 index 0000000000..11320f6952 --- /dev/null +++ b/packages/block-compiler/test/sample/BlockFileName.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/block-compiler/test/sample/BlockHead.vue b/packages/block-compiler/test/sample/BlockHead.vue new file mode 100644 index 0000000000..a07c77e3b4 --- /dev/null +++ b/packages/block-compiler/test/sample/BlockHead.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/block-compiler/test/sample/BlockMenu.vue b/packages/block-compiler/test/sample/BlockMenu.vue new file mode 100644 index 0000000000..c7f19c58e2 --- /dev/null +++ b/packages/block-compiler/test/sample/BlockMenu.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/block-compiler/test/sample/BlockTest.vue b/packages/block-compiler/test/sample/BlockTest.vue new file mode 100644 index 0000000000..7e19eb3350 --- /dev/null +++ b/packages/block-compiler/test/sample/BlockTest.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/block-compiler/test/sample/slotModelValueTest.vue b/packages/block-compiler/test/sample/slotModelValueTest.vue new file mode 100644 index 0000000000..7f5645d329 --- /dev/null +++ b/packages/block-compiler/test/sample/slotModelValueTest.vue @@ -0,0 +1,89 @@ + + + + diff --git a/packages/block-compiler/tsconfig.json b/packages/block-compiler/tsconfig.json new file mode 100644 index 0000000000..5e369f2d5c --- /dev/null +++ b/packages/block-compiler/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "emitDeclarationOnly": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/block-compiler/tsconfig.node.json b/packages/block-compiler/tsconfig.node.json new file mode 100644 index 0000000000..1020d544b9 --- /dev/null +++ b/packages/block-compiler/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "emitDeclarationOnly": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts", ".eslintrc.cjs"] +} diff --git a/packages/block-compiler/vite.config.ts b/packages/block-compiler/vite.config.ts new file mode 100644 index 0000000000..c382ee2741 --- /dev/null +++ b/packages/block-compiler/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import dts from 'vite-plugin-dts' +import path from 'node:path' +import nodeGlobalsPolyfillPluginCjs from '@esbuild-plugins/node-globals-polyfill' +import nodeModulesPolyfillPluginCjs from '@esbuild-plugins/node-modules-polyfill' + +// @ts-ignore +const nodeGlobalsPolyfillPlugin = nodeGlobalsPolyfillPluginCjs.default +// @ts-ignore +const nodeModulesPolyfillPlugin = nodeModulesPolyfillPluginCjs.default + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [dts({ rollupTypes: true, tsconfigPath: './tsconfig.json' }), vue()], + optimizeDeps: { + esbuildOptions: { + plugins: [ + nodeGlobalsPolyfillPlugin({ + process: true, + buffer: true + }), + nodeModulesPolyfillPlugin() + ] + } + }, + build: { + lib: { + entry: path.resolve(__dirname, './src/index.ts'), + name: 'block-compiler', + fileName: (_format, entryName) => `${entryName}.js`, + formats: ['es'] + }, + rollupOptions: { + external: ['@babel/core', '@vue/babel-plugin-jsx', 'vue', 'vue/compiler-sfc'] + } + } +}) diff --git a/packages/blockToWebComponentTemplate/package.json b/packages/blockToWebComponentTemplate/package.json index 9116548583..5c6104fcf9 100644 --- a/packages/blockToWebComponentTemplate/package.json +++ b/packages/blockToWebComponentTemplate/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-block-build", - "version": "2.0.0-rc.4", + "version": "2.1.0", "description": "translate block to webcomponent template", "main": "./dist/web-components.es.js", "type": "module", diff --git a/packages/build/vite-config/package.json b/packages/build/vite-config/package.json index ac2368b9bd..654ed3461f 100644 --- a/packages/build/vite-config/package.json +++ b/packages/build/vite-config/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-vite-config", - "version": "2.0.0-rc.4", + "version": "2.1.0", "description": "", "type": "module", "main": "./index.js", diff --git a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js index f9610e3d94..e53734e21e 100644 --- a/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js +++ b/packages/build/vite-config/src/vite-plugins/devAliasPlugin.js @@ -61,7 +61,8 @@ const getDevAlias = (useSourceAlias) => { '@opentiny/tiny-engine-builtin-component': path.resolve(basePath, 'packages/builtinComponent/index.js'), '@opentiny/tiny-engine-meta-register': path.resolve(basePath, 'packages/register/src/index.js'), '@opentiny/tiny-engine-layout': path.resolve(basePath, 'packages/layout/index.js'), - '@opentiny/tiny-engine-configurator': path.resolve(basePath, 'packages/configurator/src/index.js') + '@opentiny/tiny-engine-configurator': path.resolve(basePath, 'packages/configurator/src/index.js'), + '@opentiny/tiny-engine-block-compiler': path.resolve(basePath, 'packages/block-compiler/src/index.ts') } } diff --git a/packages/build/vite-plugin-meta-comments/package.json b/packages/build/vite-plugin-meta-comments/package.json index 36ca792ed2..2853da54c0 100644 --- a/packages/build/vite-plugin-meta-comments/package.json +++ b/packages/build/vite-plugin-meta-comments/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-vite-plugin-meta-comments", - "version": "2.0.0-rc.4", + "version": "2.1.0", "description": "", "type": "module", "main": "dist/index.cjs", diff --git a/packages/builtinComponent/index.js b/packages/builtinComponent/index.js index 99242b77a4..d9976790dd 100644 --- a/packages/builtinComponent/index.js +++ b/packages/builtinComponent/index.js @@ -1,4 +1,6 @@ export { default as CanvasCol } from './src/components/CanvasCol.vue' export { default as CanvasRow } from './src/components/CanvasRow.vue' export { default as CanvasRowColContainer } from './src/components/CanvasRowColContainer.vue' +export { default as CanvasFlexBox } from './src/components/CanvasFlexBox.vue' +export { default as CanvasSection } from './src/components/CanvasSection.vue' export { default as meta } from './src/meta' diff --git a/packages/builtinComponent/package.json b/packages/builtinComponent/package.json index 4248a2a535..3869da894f 100644 --- a/packages/builtinComponent/package.json +++ b/packages/builtinComponent/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-builtin-component", - "version": "2.0.0-rc.4", + "version": "2.1.0", "description": "", "main": "dist/index.mjs", "module": "dist/index.mjs", diff --git a/packages/builtinComponent/src/components/CanvasFlexBox.vue b/packages/builtinComponent/src/components/CanvasFlexBox.vue new file mode 100644 index 0000000000..593443dc6b --- /dev/null +++ b/packages/builtinComponent/src/components/CanvasFlexBox.vue @@ -0,0 +1,55 @@ + + + + diff --git a/packages/builtinComponent/src/components/CanvasSection.vue b/packages/builtinComponent/src/components/CanvasSection.vue new file mode 100644 index 0000000000..2d2994fa3f --- /dev/null +++ b/packages/builtinComponent/src/components/CanvasSection.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/builtinComponent/src/meta/CanvasFlexBox.json b/packages/builtinComponent/src/meta/CanvasFlexBox.json new file mode 100644 index 0000000000..625a3ac672 --- /dev/null +++ b/packages/builtinComponent/src/meta/CanvasFlexBox.json @@ -0,0 +1,221 @@ +{ + "component": { + "icon": "Box", + "name": { + "zh_CN": "弹性容器" + }, + "component": "CanvasFlexBox", + "schema": { + "slots": {}, + "properties": [ + { + "label": { + "zh_CN": "基础信息" + }, + "description": { + "zh_CN": "基础信息" + }, + "collapse": { + "number": 6, + "text": { + "zh_CN": "显示更多" + } + }, + "content": [ + { + "property": "flexDirection", + "type": "String", + "defaultValue": "row", + "bindState": true, + "label": { + "text": { + "zh_CN": "排列方向" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "SelectConfigurator", + "props": { + "options": [ + { + "label": "水平,起点在左端", + "value": "row" + }, + { + "label": "水平,起点在右端", + "value": "row-reverse" + }, + { + "label": "垂直,起点在上沿", + "value": "column" + }, + { + "label": "垂直,起点在下沿", + "value": "column-reverse" + } + ] + } + } + }, + { + "property": "gap", + "defaultValue": "8px", + "label": { + "text": { + "zh_CN": "间距" + } + }, + "widget": { + "component": "InputConfigurator" + }, + "description": { + "zh_CN": "控制容器内水平和垂直的间距" + }, + "labelPosition": "left" + }, + { + "property": "padding", + "defaultValue": "8px", + "label": { + "text": { + "zh_CN": "内边距" + } + }, + "widget": { + "component": "InputConfigurator" + }, + "labelPosition": "left" + }, + { + "property": "justifyContent", + "type": "String", + "defaultValue": "flex-start", + "bindState": true, + "label": { + "text": { + "zh_CN": "水平对齐方式" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "SelectConfigurator", + "props": { + "options": [ + { + "label": "左对齐", + "value": "flex-start" + }, + { + "label": "右对齐", + "value": "flex-end" + }, + { + "label": "居中", + "value": "center" + }, + { + "label": "两端对齐,子元素间隔相等", + "value": "space-between" + }, + { + "label": "子元素两侧间隔相等", + "value": "space-around" + } + ] + } + } + }, + { + "property": "alignItems", + "type": "String", + "defaultValue": "center", + "bindState": true, + "label": { + "text": { + "zh_CN": "垂直对齐方式" + } + }, + "cols": 12, + "rules": [], + "widget": { + "component": "SelectConfigurator", + "props": { + "options": [ + { + "label": "交叉轴的中点对齐", + "value": "center" + }, + { + "label": "交叉轴的起点对齐", + "value": "flex-start" + }, + { + "label": "交叉轴的终点对齐", + "value": "flex-end" + }, + { + "label": "以子元素第一行文字的基线对齐", + "value": "baseline" + }, + { + "label": "占满容器高度", + "value": "stretch" + } + ] + } + } + } + ] + } + ], + "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": [] + } + } + }, + "snippet": { + "name": { + "zh_CN": "弹性容器" + }, + "screenshot": "", + "snippetName": "CanvasFlexBox", + "icon": "Box", + "schema": { + "componentName": "CanvasFlexBox", + "props": { + "flexDirection": "row", + "gap": "8px", + "padding": "8px" + } + } + } +} diff --git a/packages/builtinComponent/src/meta/CanvasRowColContainer.json b/packages/builtinComponent/src/meta/CanvasRowColContainer.json index 3281b14a0e..4609c76b21 100644 --- a/packages/builtinComponent/src/meta/CanvasRowColContainer.json +++ b/packages/builtinComponent/src/meta/CanvasRowColContainer.json @@ -68,21 +68,21 @@ "schema": { "componentName": "CanvasRowColContainer", "props": { - "rowGap": "20px" + "rowGap": "16px" }, "children": [ { "componentName": "CanvasRow", "props": { - "rowGap": "20px", - "colGap": "20px" + "rowGap": "16px", + "colGap": "16px" }, "children": [ { "componentName": "CanvasCol", "props": { - "rowGap": "20px", - "colGap": "20px", + "rowGap": "16px", + "colGap": "16px", "grow": true, "shrink": true, "widthType": "auto" diff --git a/packages/builtinComponent/src/meta/CanvasSection.json b/packages/builtinComponent/src/meta/CanvasSection.json new file mode 100644 index 0000000000..44c2ffd837 --- /dev/null +++ b/packages/builtinComponent/src/meta/CanvasSection.json @@ -0,0 +1,71 @@ +{ + "component": { + "icon": "Box", + "name": { + "zh_CN": "全宽居中布局" + }, + "component": "CanvasSection", + "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": [] + } + } + }, + "snippet": { + "name": { + "zh_CN": "全宽居中布局" + }, + "screenshot": "", + "snippetName": "CanvasSection", + "icon": "Box", + "schema": { + "componentName": "CanvasSection", + "props": {} + } + } +} diff --git a/packages/builtinComponent/src/meta/index.js b/packages/builtinComponent/src/meta/index.js index 2480e70515..7521af69aa 100644 --- a/packages/builtinComponent/src/meta/index.js +++ b/packages/builtinComponent/src/meta/index.js @@ -1,16 +1,24 @@ import CanvasCol from './CanvasCol.json' import CanvasRow from './CanvasRow.json' import CanvasRowColContainer from './CanvasRowColContainer.json' +import CanvasFlexBox from './CanvasFlexBox.json' +import CanvasSection from './CanvasSection.json' export default { - components: [CanvasCol.component, CanvasRow.component, CanvasRowColContainer.component], + components: [ + CanvasCol.component, + CanvasRow.component, + CanvasRowColContainer.component, + CanvasFlexBox.component, + CanvasSection.component + ], snippets: [ { group: 'layout', label: { zh_CN: '布局与容器' }, - children: [CanvasRowColContainer.snippet] + children: [CanvasRowColContainer.snippet, CanvasFlexBox.snippet, CanvasSection.snippet] } ] } diff --git a/packages/canvas/DesignCanvas/README.md b/packages/canvas/DesignCanvas/README.md new file mode 100644 index 0000000000..044efb6936 --- /dev/null +++ b/packages/canvas/DesignCanvas/README.md @@ -0,0 +1,195 @@ +# schema 元服务相关API(Experimental) + +## 直接修改 schema 引用 & 调用通知更新 + +使用示例: + +```javascript +import { useCanvas, useMessage } from '@opentiny/tiny-engine-meta-register' + +const pageSchema = useCanvas().getPageSchema() + +pageSchema.css = "xxxx" + +useMessage().publish({ topic: 'schemaChange' }) +``` + +注意:直接修改 schema 引用当前不能涉及到节点的增加、删除,不然会节点树 nodesMap 无法更新,导致画布无法选中新增的组件。 + + +> 注意:以下所有 API 皆为 Experimental 实验 API,请不要用在生产阶段 + +## 导入/导出 schema + +> 这里的导入导出仅包含页面级别,不包含应用级别 schema + +**导入 schema:** + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +const data = { /*页面/区块 schema*/ } + +useCanvas().importSchema(data) +``` + +**导出 schema:** + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().exportSchema() +``` + +## 页面 schema相关操作 + +> 主要描述对页面 schema 的增删查改操作 + +### 获取当前页面/区块 schema: + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().getPageSchema() +``` + +### 获取当前选中节点 schema: + +```javascript +import { useProperties } from '@opentiny/tiny-engine-meta-register' + +const schema = useProperties().getSchema() +``` + +### 根据 id 查询对应的 节点schema(schema 片段) + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +const schema = useCanvas().getNode('453254', false) +``` + +类型: + +```typescript +/** + * 根据节点 id 获取 schema 片段 + * id: schema id + * parent: 是否需要同时获取 parent 节点 + */ +type getNode = (id: string, parent: boolean) => INode | { node: INode; parent: INode } +``` + +### 节点操作 + +#### 插入节点 + +使用示例: + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().operateNode({ + type: 'insert', + parentId: '432423', + newNodeData: { componentName: 'div', props: {}, children: [] }, + position: 'after', + referTargetNodeId: '898432' +}) +``` + +类型: + +```typescript +interface IInsertOperation { + // 操作类型为 insert + type: 'insert'; + // 要插入的节点的 父节点 id + parentId: string; + // 新节点数据 + newNodeData: INode; + // 相对节点的 id,比如我们想要插入父节点 id 中 第 5 个 children 的后面,或者前面 + referTargetNodeId: string; + // 相对节点的位置 + position: 'after' | 'before'; +} +``` + +#### 删除节点 + +使用示例: + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().operateNode({ + type: 'delete', + id: '432423' +}) +``` + +类型: + +```typescript +interface IDeleteOperation { + type: 'delete'; + id: string; +} +``` + +#### 修改节点 props + +使用示例: + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().operateNode({ + type: 'changeProps', + id: '432423', + value: { text: 'TinyEngine' }, + option: { overwrite: false } +}) +``` + +类型: + +```typescript +interface IChangePropsOperation { + type: 'changeProps'; + // 节点 id + id: string; + // 新的 props 值 + value: Record; + // 操作类型:是否覆写 + option: { overwrite: boolean; } +} +``` + +#### 更新节点属性 + +使用示例: + +```javascript +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +useCanvas().operateNode({ + type: 'updateAttributes', + id: '432423', + value: { props: { ... }, loop: { ... } }, + overwrite: boolean +}) +``` + +类型: + +```typescript +interface IUpdateAttrOperation { + type: 'updateAttributes'; + id: string; + // 对节点的属性修改 + value: Record; + // 是否是直接覆盖 + overwrite: boolean; +} +``` diff --git a/packages/canvas/DesignCanvas/src/DesignCanvas.vue b/packages/canvas/DesignCanvas/src/DesignCanvas.vue index e4bbe4708b..3aabe63e83 100644 --- a/packages/canvas/DesignCanvas/src/DesignCanvas.vue +++ b/packages/canvas/DesignCanvas/src/DesignCanvas.vue @@ -36,7 +36,8 @@ import { getOptions, getMetaApi, META_SERVICE, - META_APP + META_APP, + useNotify } from '@opentiny/tiny-engine-meta-register' import { constants } from '@opentiny/tiny-engine-utils' import * as ast from '@opentiny/tiny-engine-common/js/ast' @@ -72,7 +73,7 @@ export default { const removeNode = (node) => { const { pageState } = useCanvas() - footData.value = useCanvas().canvasApi.value.getNodePath(node?.id) + footData.value = useCanvas().getNodePath(node?.id) pageState.currentSchema = {} pageState.properties = null } @@ -102,10 +103,11 @@ export default { // 1. 页面或区块状态是未保存状态(尝试编辑) // 2. 页面刷新或第一次进入页面(含从别的页面或区块切换到别的页面或区块) // 3. 页面上已经有弹窗,不允许重复弹窗 + // 4. 当前历史堆栈为0,且当前未保存状态和上一次未保存状态不一致,不重复弹窗 const showConfirm = !isSaved || pageSchema !== oldPageSchema - if (!showConfirm || showModal) { + if (!showConfirm || showModal || (useHistory().historyState?.index === 0 && isSaved !== oldIsSaved)) { return } @@ -129,7 +131,6 @@ export default { useModal().confirm({ title: '提示', message: renderMsg, - status: 'info', exec: callback, cancel: callback, hide: () => { @@ -139,19 +140,21 @@ export default { } ) - const nodeSelected = (node, parent, type) => { + const nodeSelected = (node, parent, type, id) => { const { toolbars } = useLayout().layoutState if (type !== 'clickTree') { useLayout().closePlugin() } - const { getSchema, getNodePath } = useCanvas().canvasApi.value + const { getSchema, getNodePath } = useCanvas() + const schemaItem = useCanvas().getNodeById(id) + + const pageSchema = getSchema() - const schema = getSchema() // 如果选中的节点是画布,就设置成默认选中最外层schema - useProperties().getProps(node || schema, parent) - useCanvas().setCurrentSchema(node || schema) - footData.value = getNodePath(node?.id) + useProperties().getProps(schemaItem || pageSchema, parent) + useCanvas().setCurrentSchema(schemaItem || pageSchema) + footData.value = getNodePath(schemaItem?.id) toolbars.visiblePopover = false } @@ -218,13 +221,16 @@ export default { // 需要在canvas/render或内置组件里使用的方法 getMaterial: useMaterial().getMaterial, addHistory: useHistory().addHistory, - registerBlock: useMaterial().registerBlock, request: getMetaApi(META_SERVICE.Http).getHttp(), getPageById: getMetaApi(META_APP.AppManage).getPageById, getPageAncestors: usePage().getAncestors, getBaseInfo: () => getMetaApi(META_SERVICE.GlobalService).getBaseInfo(), addHistoryDataChangedCallback, - ast + ast, + getBlockByName: useMaterial().getBlockByName, + useModal, + useMessage, + useNotify }, isBlock, CanvasLayout, diff --git a/packages/canvas/DesignCanvas/src/api/useCanvas.js b/packages/canvas/DesignCanvas/src/api/useCanvas.js index 2be05169b7..e025fd487d 100644 --- a/packages/canvas/DesignCanvas/src/api/useCanvas.js +++ b/packages/canvas/DesignCanvas/src/api/useCanvas.js @@ -11,11 +11,14 @@ */ /* eslint-disable no-new-func */ -import { reactive, ref } from 'vue' -import { constants } from '@opentiny/tiny-engine-utils' -import { useHistory } from '@opentiny/tiny-engine-meta-register' +import { reactive, ref, toRaw } from 'vue' +import * as jsonDiffPatch from 'jsondiffpatch' +import DiffMatchPatch from 'diff-match-patch' +import { constants, utils } from '@opentiny/tiny-engine-utils' +import { useHistory, getMetaApi, useMessage } from '@opentiny/tiny-engine-meta-register' const { COMPONENT_NAME } = constants +const { deepClone } = utils const defaultPageState = { currentVm: null, @@ -53,6 +56,7 @@ const defaultSchema = { const canvasApi = ref({}) const isCanvasApiReady = ref(false) +const nodesMap = ref(new Map()) const initCanvasApi = (newCanvasApi) => { canvasApi.value = newCanvasApi @@ -60,11 +64,83 @@ const initCanvasApi = (newCanvasApi) => { } const pageState = reactive({ ...defaultPageState, loading: true }) +const rootSchema = ref([ + { + id: 0, + componentName: 'div', + props: pageState.pageSchema?.props || {}, + children: pageState.pageSchema?.children || [] + } +]) + +const generateNodesMap = (nodes, parent) => { + nodes.forEach((nodeItem) => { + if (!nodeItem.id) { + nodeItem.id = utils.guid() + } + + nodesMap.value.set(nodeItem.id, { + node: nodeItem, + parent + }) + + if (Array.isArray(nodeItem.children) && nodeItem.children.length) { + generateNodesMap(nodeItem.children, nodeItem) + } + }) +} + +const jsonDiffPatchInstance = jsonDiffPatch.create({ + objectHash: function (obj, index) { + return obj.fileName || obj.id || `$$index:${index}` + }, + arrays: { + detectMove: true, + includeValueOnMove: false + }, + textDiff: { + diffMatchPatch: DiffMatchPatch, + minLength: 60 + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + propertyFilter: function (name, context) { + return name.slice(0, 1) !== '$' + }, + cloneDiffValues: false +}) + +const { publish } = useMessage() + // 重置画布数据 const resetCanvasState = async (state = {}) => { + const previousSchema = JSON.parse(JSON.stringify(pageState.pageSchema)) + Object.assign(pageState, defaultPageState, state) - await canvasApi.value?.setSchema(pageState.pageSchema) + nodesMap.value.clear() + + if (pageState.pageSchema) { + if (!pageState.pageSchema.children) { + pageState.pageSchema.children = [] + } + + rootSchema.value = [ + { + id: 0, + componentName: 'div', + props: pageState.pageSchema.props || {}, + children: pageState.pageSchema.children + } + ] + + nodesMap.value.set(0, { node: rootSchema.value, parent: pageState.pageSchema }) + + generateNodesMap(pageState.pageSchema.children, pageState.pageSchema) + } + + const diffPatch = jsonDiffPatchInstance.diff(previousSchema, pageState.pageSchema) + + publish({ topic: 'schemaImport', data: { current: pageState.pageSchema, previous: previousSchema, diffPatch } }) } // 页面重置画布数据 @@ -80,11 +156,17 @@ const resetBlockCanvasState = async (state = {}) => { await resetCanvasState(state) } -const getDefaultSchema = (componentName = 'Page', fileName = '') => ({ - ...defaultSchema, - componentName, - fileName -}) +const getDefaultSchema = (componentName = 'Page', fileName = '') => { + const DEFAULT_PAGE = getMetaApi('engine.service.page')?.getDefaultPage() || { page_content: { props: {}, css: '' } } + + return { + ...defaultSchema, + props: DEFAULT_PAGE.page_content?.props || {}, + css: DEFAULT_PAGE.page_content?.css || '', + componentName, + fileName + } +} const setSaved = (flag = false) => { pageState.isSaved = flag @@ -97,10 +179,13 @@ const clearCanvas = () => { const { fileName, componentName } = pageState.pageSchema || {} resetCanvasState({ - pageSchema: { ...getDefaultSchema(componentName, fileName) } + pageSchema: { ...deepClone(getDefaultSchema(componentName, fileName)) } }) setSaved(false) + + canvasApi.value?.clearSelect?.() + canvasApi.value?.updateRect?.() } const isBlock = () => pageState.isBlock @@ -109,12 +194,12 @@ const isBlock = () => pageState.isBlock const initData = (schema = { ...defaultSchema }, currentPage) => { if (schema.componentName === COMPONENT_NAME.Block) { resetBlockCanvasState({ - pageSchema: schema, + pageSchema: toRaw(schema), loading: false }) } else { resetPageCanvasState({ - pageSchema: schema, + pageSchema: toRaw(schema), currentPage, loading: false }) @@ -145,6 +230,326 @@ const clearCurrentState = () => { } const getCurrentPage = () => pageState.currentPage +const getNodeById = (id) => { + return nodesMap.value.get(id)?.node +} + +const getNodeWithParentById = (id) => { + return nodesMap.value.get(id) +} + +const delNode = (id) => { + nodesMap.value.delete(id) +} + +const clearNodes = () => { + nodesMap.value.clear() +} + +const setNode = (schema, parent) => { + schema.id = schema.id || utils.guid() + + nodesMap.value.set(schema.id, { node: schema, parent }) +} + +const getNode = (id, parent) => { + return parent ? nodesMap.value.get(id) : nodesMap.value.get(id)?.node +} + +const operationTypeMap = { + insert: (operation) => { + const { parentId, newNodeData, position, referTargetNodeId } = operation + const parentNode = getNode(parentId) || pageState.pageSchema + + if (!parentNode) { + return {} + } + + parentNode.children = parentNode.children || [] + + if (!newNodeData.id) { + newNodeData.id = utils.guid() + } + + if (referTargetNodeId) { + const referenceNode = getNode(referTargetNodeId) + let index = parentNode.children.indexOf(referenceNode) + + if (index === -1) { + index = 0 + } + + index = position === 'before' ? index : index + 1 + + parentNode.children.splice(index, 0, newNodeData) + + setNode(newNodeData, parentNode) + + // 递归构建 nodeMap + if (Array.isArray(newNodeData?.children) && newNodeData.children.length) { + const newNode = getNode(newNodeData.id) + generateNodesMap(newNodeData.children, newNode) + } + + return { + current: newNodeData, + previous: undefined + } + } + + if (position === 'before') { + parentNode.children.unshift(newNodeData) + } else { + parentNode.children.push(newNodeData) + } + + setNode(newNodeData, parentNode) + + // 递归构建 nodeMap + if (Array.isArray(newNodeData?.children) && newNodeData.children.length) { + const newNode = getNode(newNodeData.id) + generateNodesMap(newNodeData.children, newNode) + } + + return { + current: newNodeData, + previous: undefined + } + }, + delete: (operation) => { + const { id } = operation + const targetNode = getNode(id, true) + + if (!targetNode) { + return + } + + const { parent, node } = targetNode + + const index = parent.children.indexOf(node) + + if (index > -1) { + parent.children.splice(index, 1) + nodesMap.value.delete(node.id) + } + + let children = [...(node.children || [])] + + // 递归清理 nodesMap + while (children?.length) { + const len = children.length + children.forEach((item) => { + const nodeItem = getNode(item.id) + nodesMap.value.delete(item.id) + + if (Array.isArray(nodeItem.children) && nodeItem.children.length) { + children.push(...nodeItem.children) + } + }) + + children = children.slice(len) + } + + return { + current: undefined, + previous: node + } + }, + changeProps: (operation) => { + const { id, value, option: changeOption } = operation + let { node } = getNode(id, true) || {} + const previous = deepClone(node) + const { overwrite = false } = changeOption || {} + + if (!node) { + node = pageState.pageSchema + } + + if (!node.props) { + node.props = {} + } + + if (overwrite) { + node.props = value.props + } else { + Object.assign(node.props, value?.props || {}) + } + + return { + current: node, + previous + } + }, + updateAttributes: (operation) => { + const { id, value, overwrite } = operation + const { id: _id, children, ...restAttr } = value + const node = getNode(id) + + // 其他属性直接浅 merge + Object.assign(node, restAttr) + + // 配置了 overwrite,需要将没有传入的属性进行删除(不包括 children) + if (overwrite) { + const { id: _unUsedId, children: _unUsedChildren, ...restOrigin } = node + const originKeys = Object.keys(restOrigin) + const newKeysSet = new Set(Object.keys(restAttr)) + + originKeys.forEach((key) => { + if (!newKeysSet.has(key)) { + delete node[key] + } + }) + } + + if (!Array.isArray(children)) { + // 非数组类型的 children,比如是直接的字符串作为 children + if (children || typeof children === 'string') { + node.children = children + } + + return + } + + const newChildren = children.map((item) => { + if (!item.id) { + item.id = utils.guid() + } + + return item + }) + // 传了 children 进来,需要找出来被删除的、新增的,剩下的是修改的。 + const originChildrenIds = (node.children || []).filter(({ id }) => id).map(({ id }) => id) + const originChildrenSet = new Set(originChildrenIds) + + const newChildrenSet = new Set(newChildren.map(({ id }) => id)) + // 被删除的项 + const deletedIds = originChildrenIds.filter((id) => !newChildrenSet.has(id)) + const deletedIdsSet = new Set(deletedIds) + + for (const id of deletedIds) { + operationTypeMap.delete({ id }) + } + + // 筛选出来新增的和修改的 + const changedChildren = newChildren.filter(({ id }) => !deletedIdsSet.has(id)) + + changedChildren.forEach((childItem, index) => { + // 新增 + if (!originChildrenSet.has(childItem.id)) { + operationTypeMap.insert({ + parentId: id, + newNodeData: childItem, + position: 'after', + referTargetNodeId: changedChildren?.[index]?.id + }) + return + } + + // 直接改引用插入进来,但是没有构建对应的 Map,需要构建Map + if (!getNode(childItem.id)) { + setNode(childItem, node) + + // 递归构建 nodeMap + if (Array.isArray(childItem?.children) && childItem.children.length) { + const newNode = getNode(childItem.id) + generateNodesMap(childItem.children, newNode) + } + } + + // 递归修改 + operationTypeMap.updateAttributes({ id: childItem.id, value: childItem }) + }) + } +} + +const lastUpdateType = ref('') + +/** + * @experimental + * @param {*} operation + * @returns + */ +const operateNode = async (operation) => { + if (!operationTypeMap[operation.type]) { + return + } + + operationTypeMap[operation.type](operation) + + lastUpdateType.value = operation.type + + publish({ topic: 'schemaChange', data: { operation } }) + + if (operation.type !== 'insert') { + // 这里 setTimeout 延时是需要等画布更新渲染完成,然后再更新,才能得到正确的组件 offset + setTimeout(() => { + canvasApi.value.updateRect?.() + }, 0) + } +} + +// 获取传入的 schema 与最新 schema 的 diff +const getSchemaDiff = (schema) => { + return jsonDiffPatchInstance.diff(schema, pageState.pageSchema) +} + +const patchLatestSchema = (schema) => { + // 这里 pageSchema 需要 deepClone,不然 patch 的时候,会 patch 成同一个引用,造成画布无法更新 + const diff = jsonDiffPatchInstance.diff(schema, deepClone(pageState.pageSchema)) + + if (diff) { + jsonDiffPatchInstance.patch(schema, diff) + } +} + +const importSchema = (data) => { + let importData = data + + if (typeof data === 'string') { + try { + importData = JSON.parse(data) + } catch (error) { + // eslint-disable-next-line no-console + console.error('[useCanvas.importSchema] Invalid data') + } + } + + // JSON 格式校验 + resetCanvasState({ + pageSchema: importData + }) + + canvasApi.value?.clearSelect?.() +} + +const exportSchema = () => { + return JSON.stringify(pageState.pageSchema) +} + +const getSchema = () => { + return pageState.pageSchema || {} +} + +const getNodePath = (id, nodes = []) => { + const { parent, node } = getNodeWithParentById(id) || {} + + node && nodes.unshift({ name: node.componentName, node: id }) + + if (parent) { + parent && getNodePath(parent.id, nodes) + } else { + nodes.unshift({ name: 'BODY', node: id }) + } + + return nodes +} + +const updateSchema = (data) => { + Object.assign(pageState.pageSchema, data) + + publish({ topic: 'schemaChange', data: {} }) +} + export default function () { return { pageState, @@ -163,6 +568,21 @@ export default function () { getCurrentPage, initCanvasApi, canvasApi, - isCanvasApiReady + isCanvasApiReady, + getNodeById, + getNodeWithParentById, + delNode, + clearNodes, + setNode, + getNode, + operateNode, + lastUpdateType, + getSchemaDiff, + patchLatestSchema, + importSchema, + exportSchema, + getSchema, + getNodePath, + updateSchema } } diff --git a/packages/canvas/DesignCanvas/src/importMap.js b/packages/canvas/DesignCanvas/src/importMap.js index 813fdb7dc1..9d8d4c536b 100644 --- a/packages/canvas/DesignCanvas/src/importMap.js +++ b/packages/canvas/DesignCanvas/src/importMap.js @@ -14,9 +14,14 @@ export function getImportMapData(overrideVersions = {}) { const blockRequire = { imports: { '@opentiny/vue': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue.mjs`, - '@opentiny/vue-icon': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-icon.mjs` + '@opentiny/vue-icon': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-icon.mjs`, + 'element-plus': `${VITE_CDN_DOMAIN}/element-plus@2.4.2/dist/index.full.mjs`, + '@opentiny/tiny-engine-builtin-component': `${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-builtin-component@^2.0.0/dist/index.mjs` }, - importStyles: [`${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`] + importStyles: [ + `${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`, + `${VITE_CDN_DOMAIN}/element-plus@2.4.2/dist/index.css` + ] } // 以下内容由于物料协议不支持声明子依赖而@opentiny/vue需要依赖所以需要补充 diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index 74cd1c21b6..39ede9acfc 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -29,9 +29,9 @@ + + diff --git a/packages/canvas/render/src/BlockLoading.vue b/packages/canvas/render/src/BlockLoading.vue new file mode 100644 index 0000000000..b74bebdc27 --- /dev/null +++ b/packages/canvas/render/src/BlockLoading.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/canvas/render/src/RenderMain.ts b/packages/canvas/render/src/RenderMain.ts index ff6a4e303f..0f401599b7 100644 --- a/packages/canvas/render/src/RenderMain.ts +++ b/packages/canvas/render/src/RenderMain.ts @@ -11,27 +11,22 @@ */ import { provide, watch, defineComponent, PropType, ref, inject, onUnmounted, h, Ref } from 'vue' - -import { useBroadcastChannel } from '@vueuse/core' -import { constants } from '@opentiny/tiny-engine-utils' - import { getDesignMode, setDesignMode, setController, useCustomRenderer, getController } from './canvas-function' -import { setConfigure } from './material-function' +import { removeBlockCompsCache, setConfigure } from './material-function' import { useUtils, useBridge, useDataSourceMap, useGlobalState } from './application-function' import { IPageSchema, useContext, usePageContext, useSchema } from './page-block-function' import { api, setCurrentApi } from './canvas-function/canvas-api' import { getPageAncestors } from './material-function/page-getter' import CanvasEmpty from './canvas-function/CanvasEmpty.vue' import { setCurrentPage } from './canvas-function/page-switcher' - -const { BROADCAST_CHANNEL } = constants +import { useThrottleFn } from '@vueuse/core' // global-context singleton const { context: globalContext, setContext: setGlobalContext } = useContext() -const { refreshKey, utils, getUtils, setUtils, updateUtils, deleteUtils } = useUtils(globalContext) -const { bridge, setBridge, getBridge } = useBridge() +const { refreshKey, utils, getUtils, setUtils } = useUtils(globalContext) +const { bridge } = useBridge() const { getDataSourceMap, setDataSourceMap } = useDataSourceMap() -const { getGlobalState, setGlobalState, stores } = useGlobalState() +const { setGlobalState, stores } = useGlobalState() const updateGlobalContext = () => { const context = { utils, @@ -46,20 +41,12 @@ const updateGlobalContext = () => { setGlobalContext(context, true) } updateGlobalContext() -const activePageContext = usePageContext() +export const activePageContext = usePageContext() const { schema: activeSchema, - getSchema, setSchema, - getState, - setState, - deleteState, - getProps, - setProps, - getMethods, - setMethods, - setPagecss + setPageCss } = useSchema(activePageContext, { utils, bridge, @@ -67,44 +54,31 @@ const { getDataSourceMap }) const { getRenderer, setRenderer } = useCustomRenderer() -const getNode = (id, parent) => (id ? activePageContext.getNode(id, parent) : activeSchema) -const { getContext, getRoot, setNode, setCondition, getCondition, getConditions } = activePageContext +const { setCondition } = activePageContext +const updateCanvas = () => { + refreshKey.value++ +} setCurrentApi({ 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, getDesignMode, - setDesignMode + setDesignMode, + removeBlockCompsCache, + updateCanvas }) +const throttleUpdateSchema = useThrottleFn( + () => { + window.host.patchLatestSchema(activeSchema) + }, + 100, + true +) + export default defineComponent({ props: { entry: { @@ -162,18 +136,83 @@ export default defineComponent({ onUnmounted(() => { cancel() }) + + window.host.subscribe({ + topic: 'schemaChange', + subscriber: 'canvasRenderer', + callback: throttleUpdateSchema + }) + + window.host.subscribe({ + topic: 'schemaImport', + subscriber: 'canvasRenderer', + callback: () => { + setSchema(window.host.getSchema()) + } + }) + + watch( + () => activeSchema.css, + (value) => { + setPageCss(value) + } + ) + + const utilsWatchCanceler = window.host.watch( + () => window.host.appSchema?.utils, + (data) => { + setUtils(data) + }, + { + immediate: true, + deep: true + } + ) + + const dataSourceWatchCanceler = window.host.watch( + () => window.host.appSchema?.dataSource, + (data) => { + setDataSourceMap(data) + }, + { + immediate: true, + deep: true + } + ) + + const globalStateWatchCanceler = window.host.watch( + () => window.host.appSchema?.globalState, + (data) => { + setGlobalState(data) + }, + { + immediate: true, + deep: true + } + ) + + onUnmounted(() => { + window.host.unsubscribe({ + topic: 'schemaChange', + subscriber: 'canvasRenderer' + }) + + window.host.unsubscribe({ + topic: 'schemaImport', + subscriber: 'canvasRenderer' + }) + + utilsWatchCanceler() + dataSourceWatchCanceler() + globalStateWatchCanceler() + }) } let schema = activeSchema let setCurrentSchema - let setCurrentMethod = setMethods if (pageContext.pageId && !props.active && !props.entry) { // 注意顶层使用activeSchema和对应的api - const { - schema: inActiveSchema, - setSchema: setInactiveSchema, - setMethods: setInactiveMethods - } = useSchema(pageContext, { + const { schema: inActiveSchema, setSchema: setInactiveSchema } = useSchema(pageContext, { utils, bridge, stores, @@ -181,30 +220,10 @@ export default defineComponent({ }) schema = inActiveSchema setCurrentSchema = setInactiveSchema - setCurrentMethod = setInactiveMethods } provide('rootSchema', schema) - const { post } = useBroadcastChannel({ name: BROADCAST_CHANNEL.SchemaLength }) - watch( - () => schema?.children?.length, - (length) => { - post(length) - } - ) - - // 这里监听schema.methods,为了保证methods上下文环境始终为最新 - watch( - () => schema.methods, - (value) => { - setCurrentMethod(value, true) - }, - { - deep: true - } - ) - if (!props.entry) { watch( [() => props.active, () => props.renderSchema], diff --git a/packages/canvas/render/src/application-function/bridge.ts b/packages/canvas/render/src/application-function/bridge.ts index 0acc6651c2..95ae6705b9 100644 --- a/packages/canvas/render/src/application-function/bridge.ts +++ b/packages/canvas/render/src/application-function/bridge.ts @@ -1,16 +1,7 @@ -import { reset } from '../data-utils' - export function useBridge() { const bridge = {} - const setBridge = (data, clear = false) => { - clear && reset(bridge) - Object.assign(bridge, data) - } - const getBridge = () => bridge return { - bridge, - setBridge, - getBridge + bridge } } diff --git a/packages/canvas/render/src/application-function/global-state.ts b/packages/canvas/render/src/application-function/global-state.ts index cd0d6810fe..9e8f84ae82 100644 --- a/packages/canvas/render/src/application-function/global-state.ts +++ b/packages/canvas/render/src/application-function/global-state.ts @@ -5,9 +5,6 @@ const Func = Function export function useGlobalState() { const globalState = ref([]) - const getGlobalState = () => { - return globalState.value - } const setGlobalState = (data = []) => { globalState.value = data @@ -28,7 +25,6 @@ export function useGlobalState() { }) return { globalState, - getGlobalState, setGlobalState, stores } diff --git a/packages/canvas/render/src/application-function/utils.ts b/packages/canvas/render/src/application-function/utils.ts index c972b456de..6a92d3c2ee 100644 --- a/packages/canvas/render/src/application-function/utils.ts +++ b/packages/canvas/render/src/application-function/utils.ts @@ -18,10 +18,20 @@ export function useUtils(context: Record) { const utils: Record = {} const getUtils = () => utils - const setUtils = (data: Array, clear = false, isForceRefresh = false) => { - if (clear) { - reset(utils) + const setUtils = (data: Array) => { + if (!Array.isArray(data)) { + return } + + // 筛选出来已经被删除的 key + const newKeys = new Set(data.map(({ name }) => name)) + const currentKeys = Object.keys(utils) + const deletedUtilsKeys = currentKeys.filter((item) => !newKeys.has(item)) + + for (const key of deletedUtilsKeys) { + delete utils[key] + } + const utilsCollection = {} // 目前画布还不具备远程加载utils工具类的功能,目前只能加载TinyVue组件库中的组件工具 data?.forEach((item) => { @@ -44,31 +54,13 @@ export function useUtils(context: Record) { }) Object.assign(utils, utilsCollection) - // 因为工具类并不具有响应式行为,所以需要通过修改key来强制刷新画布 - if (isForceRefresh) { - refreshKey.value++ - } - } - - const updateUtils = (data: Array) => { - setUtils(data, false, true) - } - - const deleteUtils = (data: Array) => { - data?.forEach((item) => { - if (utils[item.name]) { - delete utils[item.name] - } - }) - setUtils([], false, true) + refreshKey.value++ } return { refreshKey, utils, getUtils, - setUtils, - updateUtils, - deleteUtils + setUtils } } diff --git a/packages/canvas/render/src/builtin/CanvasCollection.js b/packages/canvas/render/src/builtin/CanvasCollection.js index b5cb227629..d5104ee996 100644 --- a/packages/canvas/render/src/builtin/CanvasCollection.js +++ b/packages/canvas/render/src/builtin/CanvasCollection.js @@ -11,8 +11,6 @@ */ import { getController } from '../render' -import { api } from '../RenderMain' -import { useModal } from '@opentiny/tiny-engine-meta-register' const NAME_PREFIX = { loop: 'loop', @@ -64,7 +62,6 @@ const removeState = (pageSchema, variableName) => { } const setStateWithSourceRef = (pageSchema, variableName, sourceRef, data) => { - api.setState({ [variableName]: data }) pageSchema.state[variableName] = data if (sourceRef.value.data?.option?.isSync) { @@ -104,7 +101,7 @@ const defaultHandlerTemplate = ({ node, sourceRef, schemaId, pageSchema }) => { } } -const generateAssginColumns = (newColumns, oldColumns) => { +const generateAssignColumns = (newColumns, oldColumns) => { newColumns.forEach((item) => { const targetColumn = oldColumns.find((value) => value.field === item.field) if (targetColumn) { @@ -114,22 +111,38 @@ const generateAssginColumns = (newColumns, oldColumns) => { 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 askShouldImportData = ({ node, sourceRef, updateKey }) => { + const { publish } = getController().useMessage() + + getController() + .useModal() + .confirm({ + message: '检测到表格存在配置的数据,是否需要引入?', + exec() { + try { + const sourceColumns = sourceRef.value?.data?.columns?.map(({ title, field }) => ({ title, field })) || [] + // 这里需要找到对应列,然后进行列合并 + node.props.columns = generateAssignColumns(sourceColumns, node.props.columns) + + publish({ topic: 'schemaChange', data: {} }) + updateKey.value++ + } catch (error) { + getController().useNotify({ + type: 'error', + message: '引入配置数据失败' + }) + } + }, + cancel() { + node.props.columns = [...(sourceRef.value.data?.columns || [])] + + publish({ topic: 'schemaChange', data: {} }) + } + }) } -const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName }) => { - if (!node || !node.props) { +const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName, updateKey }) => { + if (!node || !node.props || !sourceName) { return } @@ -137,7 +150,7 @@ const updateNodeHandler = ({ node, sourceRef, pageSchema, sourceName, methodName delete node?.props?.data if (node.props.columns.length) { - askShouldImportData({ node, sourceRef }) + askShouldImportData({ node, sourceRef, updateKey }) } else { node.props.columns = [...(sourceRef.value.data?.columns || [])] } @@ -176,7 +189,7 @@ this.dataSourceMap.${sourceName}.load().then((res) => { } const extraHandlerMap = { - TinyGrid: ({ node, sourceRef, schemaId, pageSchema }) => { + TinyGrid: ({ node, sourceRef, schemaId, pageSchema, updateKey }) => { const sourceName = sourceRef.value?.name const methodName = `${NAME_PREFIX.table}${schemaId}` @@ -185,7 +198,7 @@ const extraHandlerMap = { value: `{ api: this.${methodName} }` } - const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName }) + const updateNode = () => updateNodeHandler({ node, sourceRef, pageSchema, sourceName, methodName, updateKey }) const clearBindVar = () => { // 当数据源组件children字段为空时,及时清空创建的methods @@ -272,7 +285,7 @@ const extraHandlerMap = { } } -export const getHandler = ({ node, sourceRef, schemaId, pageSchema }) => +export const getHandler = ({ node, sourceRef, schemaId, pageSchema, updateKey }) => extraHandlerMap[node.componentName] - ? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema }) + ? extraHandlerMap[node.componentName]({ node, sourceRef, schemaId, pageSchema, updateKey }) : defaultHandlerTemplate({ node, sourceRef, schemaId, pageSchema }) diff --git a/packages/canvas/render/src/builtin/CanvasCollection.vue b/packages/canvas/render/src/builtin/CanvasCollection.vue index d033924f34..138ea36026 100644 --- a/packages/canvas/render/src/builtin/CanvasCollection.vue +++ b/packages/canvas/render/src/builtin/CanvasCollection.vue @@ -1,17 +1,16 @@ diff --git a/packages/canvas/render/src/builtin/builtin.json b/packages/canvas/render/src/builtin/builtin.json index 8597e0bebe..228e51296b 100644 --- a/packages/canvas/render/src/builtin/builtin.json +++ b/packages/canvas/render/src/builtin/builtin.json @@ -83,7 +83,7 @@ "content": [ { "property": "name", - "type": "String", + "type": "string", "label": { "text": { "zh_CN": "插槽名称" @@ -97,7 +97,7 @@ }, { "property": "params", - "type": "String", + "type": "string", "defaultValue": "", "label": { "text": { @@ -246,7 +246,7 @@ "content": [ { "property": "condition", - "type": "Boolean", + "type": "boolean", "defaultValue": true, "label": { "text": { @@ -262,7 +262,7 @@ }, { "property": "style", - "type": "String", + "type": "string", "defaultValue": "", "label": { "text": { @@ -278,7 +278,7 @@ }, { "property": "dataSource", - "type": "String", + "type": "string", "defaultValue": "", "bindState": false, "label": { @@ -332,7 +332,7 @@ "content": [ { "property": "text", - "type": "String", + "type": "string", "defaultValue": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。", "label": { "text": { @@ -404,7 +404,7 @@ "content": [ { "property": "name", - "type": "String", + "type": "string", "defaultValue": "IconDel", "bindState": true, "label": { @@ -474,7 +474,7 @@ "content": [ { "property": "src", - "type": "String", + "type": "string", "defaultValue": "", "bindState": true, "label": { @@ -557,6 +557,7 @@ "schema": { "componentName": "Text", "props": { + "style": "display: inline-block;", "text": "TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。" } } diff --git a/packages/canvas/render/src/canvas-function/custom-renderer.ts b/packages/canvas/render/src/canvas-function/custom-renderer.ts index 8b7ba8edfe..7b0d4773c7 100644 --- a/packages/canvas/render/src/canvas-function/custom-renderer.ts +++ b/packages/canvas/render/src/canvas-function/custom-renderer.ts @@ -5,6 +5,7 @@ import renderer from '../render' function defaultRenderer(schema, refreshKey, entry, active, isPage = true) { // 渲染画布增加根节点,与出码和预览保持一致 const rootChildrenSchema = { + id: 0, componentName: 'div', // 手动添加一个唯一的属性,后续在画布选中此节点时方便处理额外的逻辑。由于没有修改schema,不会影响出码 props: { ...schema.props, 'data-id': 'root-container', 'data-page-active': active }, @@ -24,6 +25,7 @@ function defaultRenderer(schema, refreshKey, entry, active, isPage = true) { } return h( + // TODO: 这里顶层的 i18n-host 在不支持 webComponent 的区块之后,应该也不需要webComponent 的 i18n provider 了 'tiny-i18n-host', { locale: 'zh_CN', diff --git a/packages/canvas/render/src/data-function/parser.ts b/packages/canvas/render/src/data-function/parser.ts index 081b33d9ad..51b277091b 100644 --- a/packages/canvas/render/src/data-function/parser.ts +++ b/packages/canvas/render/src/data-function/parser.ts @@ -3,7 +3,7 @@ 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 { collectionMethodsMap, getComponent, getIcon } from '../material-function' import { newFn } from '../data-utils' import { renderDefault } from '../render' @@ -61,8 +61,7 @@ const transformJSX = (code) => { [ babelPluginJSX, { - pragma: 'h', - isCustomElement: (name) => customElements[name] + pragma: 'h' } ] ] @@ -152,43 +151,39 @@ const parseJSXFunction = (data, _scope, ctx) => { 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 } - }) + + 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 + }) } + + return result } } const parseJSFunction = (data, _scope, ctx) => { diff --git a/packages/canvas/render/src/data-utils.ts b/packages/canvas/render/src/data-utils.ts index 867256da4e..320ac0198f 100644 --- a/packages/canvas/render/src/data-utils.ts +++ b/packages/canvas/render/src/data-utils.ts @@ -10,3 +10,10 @@ export const newFn = (...argv) => { const Fn = Function return new Fn(...argv) } + +export const getDeletedKeys = (objA, objB) => { + const keyA = Object.keys(objA) + const keyB = new Set(Object.keys(objB)) + + return keyA.filter((item) => !keyB.has(item)) +} diff --git a/packages/canvas/render/src/lowcode.ts b/packages/canvas/render/src/lowcode.ts index c3b5779030..de651513d9 100644 --- a/packages/canvas/render/src/lowcode.ts +++ b/packages/canvas/render/src/lowcode.ts @@ -14,7 +14,6 @@ import { getCurrentInstance, nextTick, provide, inject } from 'vue' import { I18nInjectionKey } from 'vue-i18n' import { api } from './RenderMain' import { globalNotify } from './canvas-function' -import { collectionMethodsMap } from './material-function' import { generateFn } from './data-function' export const lowcodeWrap = (props, context) => { @@ -60,13 +59,13 @@ export const lowcodeWrap = (props, context) => { const wrap = (fn) => { if (typeof fn === 'function') { - const fnName = fn.name - if (fn.toString().includes('return this')) { + const fnString = fn.toString() + + if (fnString.includes('return this')) { return () => global - } else if (fnName && collectionMethodsMap[fnName.slice(0, -1)]) { - // 这里区块打包的时候会在方法名称后面多加一个字符串,所以此处需要截取下函数名称 - fn.realName = fnName.slice(0, -1) - return generateFn(fn) + } else if (/this\.dataSourceMap\.[0-9a-zA-Z_]+\.load\(\)/.test(fnString)) { + const renderContext = (inject('pageContext') as Ref).value + return generateFn(fn, renderContext) } else if (fn.name === 'setter' || fn.name === 'getter') { // 这里需要保证在消费区块时,区块中的访问器函数可以正常执行 return (...args) => { diff --git a/packages/canvas/render/src/material-function/material-getter.ts b/packages/canvas/render/src/material-function/material-getter.ts index be9da1e82e..95bc1da415 100644 --- a/packages/canvas/render/src/material-function/material-getter.ts +++ b/packages/canvas/render/src/material-function/material-getter.ts @@ -1,8 +1,14 @@ -import { h } from 'vue' -import { isHTMLTag, hyphenate } from '@vue/shared' +import { h, defineAsyncComponent } from 'vue' +import { isHTMLTag } 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 { + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasFlexBox, + CanvasSection +} from '@opentiny/tiny-engine-builtin-component' import { CanvasBox, CanvasCollection, @@ -15,9 +21,8 @@ import { CanvasRouterLink } from '../builtin' import { getController } from '../canvas-function/controller' -import { generateCollection } from './support-collection' +import BlockLoadError from '../BlockLoadError.vue' -export const customElements = {} export const Mapper = { Icon: CanvasIcon, Text: CanvasText, @@ -27,6 +32,8 @@ export const Mapper = { slot: CanvasSlot, Template: CanvasBox, Img: CanvasImg, + CanvasSection, + CanvasFlexBox, CanvasRow, CanvasCol, CanvasRowColContainer, @@ -42,91 +49,62 @@ const getBlock = (name) => { return window.blocks?.[name] } -const { hyphenateRE } = utils -const getPlainProps = (object: Record = {}) => { - const { slot, ...rest } = object - const props = {} +const blockComponentsBlobUrlMap = new Map() - if (slot) { - rest.slot = slot.name || slot - } +// TODO: 这里的全局 getter 方法名,可以做成配置化 +const loadBlockComponent = async (name: string) => { + try { + if (blockComponentsBlobUrlMap.has(name)) { + return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name)) + } - Object.entries(rest).forEach(([key, value]) => { - let renderKey = key + const blocksBlob = (await getController().getBlockByName(name)) as Array<{ blobURL: string; style: string }> - // html 标签属性会忽略大小写,所以传递包含大写的 props 需要转换为 kebab 形式的 props - if (!/on[A-Z]/.test(renderKey) && hyphenateRE.test(renderKey)) { - renderKey = hyphenate(renderKey) - } + for (const [fileName, value] of Object.entries(blocksBlob)) { + blockComponentsBlobUrlMap.set(fileName, value.blobURL) + + if (!value.style) { + continue + } + + // 注册 CSS,以区块为维度 + const stylesheet = document.querySelector(`#${fileName}`) - if (['boolean', 'string', 'number'].includes(typeof value)) { - props[renderKey] = value - } else { - // 如果传给webcomponent标签的是对象或者数组需要使用.prop修饰符,转化成h函数就是如下写法 - props[`.${renderKey}`] = value + if (stylesheet) { + stylesheet.innerHTML = value.style + } else { + const newStylesheet = document.createElement('style') + newStylesheet.innerHTML = value.style + newStylesheet.setAttribute('id', fileName) + document.head.appendChild(newStylesheet) + } } - }) - return props -} -const generateBlockContent = (schema) => { - if (schema?.componentName === 'Collection') { - generateCollection(schema) + return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name)) + } catch (error) { + // 加载错误提示 + return h(BlockLoadError, { name }) } - 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) +window.loadBlockComponent = loadBlockComponent - if (!Object.keys(material).length) { - registerBlock(componentName) - } +const getBlockComponent = (name) => { + return defineAsyncComponent(() => loadBlockComponent(name)) +} - customElements[componentName] = { - name: componentName + '.ce', - render() { - return h( - hyphenate(componentName), - window.parent.TinyGlobalConfig.dslMode === 'Vue' ? getPlainProps(this.$attrs) : this.$attrs, - this.$slots.default?.() - ) - } - } +// 移除区块缓存 +export const removeBlockCompsCache = () => { + blockComponentsBlobUrlMap.forEach((_, fileName) => { + const stylesheet = document.querySelector(`#${fileName}`) + stylesheet?.remove?.() + }) - return customElements[componentName] + blockComponentsBlobUrlMap.clear() } export const getIcon = (name) => TinyVueIcon?.[name]?.() || '' export const getComponent = (name) => { - return ( - Mapper[name] || - getNative(name) || - getBlock(name) || - customElements[name] || - (isHTMLTag(name) ? name : wrapCustomElement(name)) - ) + return Mapper[name] || getNative(name) || getBlock(name) || (isHTMLTag(name) ? name : getBlockComponent(name)) } diff --git a/packages/canvas/render/src/page-block-function/context.ts b/packages/canvas/render/src/page-block-function/context.ts index 38688819e0..3958920e4e 100644 --- a/packages/canvas/render/src/page-block-function/context.ts +++ b/packages/canvas/render/src/page-block-function/context.ts @@ -11,7 +11,6 @@ */ import { shallowReactive } from 'vue' -import { utils } from '@opentiny/tiny-engine-utils' export function useContext() { const context = shallowReactive({}) @@ -29,38 +28,6 @@ export function useContext() { } } -export function useNodes() { - const nodes = {} - - const setNode = (schema, parent) => { - schema.id = schema.id || utils.guid() - nodes[schema.id] = { node: schema, parent } - } - - const getNode = (id, parent) => { - return parent ? nodes[id] : nodes[id].node - } - - const delNode = (id) => delete nodes[id] - - const clearNodes = () => { - Object.keys(nodes).forEach(delNode) - } - const getRoot = (id) => { - const { parent } = getNode(id, true) - - return parent?.id ? getRoot(parent.id) : parent - } - - return { - setNode, - getNode, - delNode, - clearNodes, - getRoot - } -} - export function useCondition() { // 从大纲树控制隐藏 const conditions = shallowReactive({}) @@ -107,13 +74,11 @@ export function useCssScopeId() { } export function usePageContext() { const contextExpose = useContext() - const nodeExpose = useNodes() const conditionExpose = useCondition() const contextParentExpose = usePageContextParent() const cssCopeIdExpose = useCssScopeId() return { ...contextExpose, - ...nodeExpose, ...conditionExpose, ...contextParentExpose, ...cssCopeIdExpose, diff --git a/packages/canvas/render/src/page-block-function/css.ts b/packages/canvas/render/src/page-block-function/css.ts index 28ed32ba15..f92bfcb0c6 100644 --- a/packages/canvas/render/src/page-block-function/css.ts +++ b/packages/canvas/render/src/page-block-function/css.ts @@ -1,6 +1,6 @@ import { initStyle } from '../material-function/page-getter' import { getController } from '../render' -export function setPagecss(css = '', pageId?) { +export function setPageCss(css = '', pageId?) { const cssPageId = pageId ?? getController().getBaseInfo().pageId const key = `data-te-page-${cssPageId}` initStyle(key, css) diff --git a/packages/canvas/render/src/page-block-function/props.ts b/packages/canvas/render/src/page-block-function/props.ts index 8d1986990b..9cc7fcde5e 100644 --- a/packages/canvas/render/src/page-block-function/props.ts +++ b/packages/canvas/render/src/page-block-function/props.ts @@ -8,8 +8,6 @@ export function useProps(generateAccessor: ReturnType['ge Object.assign(props, data) } - const getProps = () => props - const initProps = (properties = []) => { const props: Record = {} const accessorFunctions: Array> = [] @@ -38,7 +36,6 @@ export function useProps(generateAccessor: ReturnType['ge return { props, initProps, - getProps, setProps } } diff --git a/packages/canvas/render/src/page-block-function/schema.ts b/packages/canvas/render/src/page-block-function/schema.ts index f9734194b5..136f0db0b4 100644 --- a/packages/canvas/render/src/page-block-function/schema.ts +++ b/packages/canvas/render/src/page-block-function/schema.ts @@ -1,4 +1,4 @@ -import { reactive, watchEffect } from 'vue' +import { reactive, watchEffect, watch } from 'vue' import { reset } from '../data-utils' import { useAccessorMap } from './accessor-map' import { useState } from './state' @@ -6,30 +6,50 @@ import { useProps } from './props' import { useMethods } from './methods' import { nextTick } from 'vue' import { globalNotify } from '../canvas-function' -import { setPagecss } from './css' +import { setPageCss } from './css' import type { IPageSchema, ISchemaChildrenItem } from '@opentiny/tiny-engine-dsl-vue' export { IPageSchema, ISchemaChildrenItem } export function useSchema( - { context: globalContext, setContext, getContext, clearNodes, getNode }, + { context: globalContext, setContext, getContext }, { utils, bridge, stores, getDataSourceMap } ) { const schema = reactive>({}) const { generateAccessor, stateAccessorMap, propsAccessorMap, generateStateAccessors } = useAccessorMap(globalContext) - const { state, getState, setState, deleteState } = useState(schema, { + const { state, setState } = useState(schema, { getContext, generateStateAccessors }) - const { props, initProps, getProps, setProps } = useProps(generateAccessor) + const { props, initProps, setProps } = useProps(generateAccessor) const { methods, getMethods, setMethods } = useMethods({ setContext, getContext }) - const getSchema = () => schema + watch( + () => schema.state, + (value) => { + setState(value) + }, + { + deep: true + } + ) + + // 这里监听schema.methods,为了保证methods上下文环境始终为最新 + watch( + () => schema.methods, + (value) => { + setMethods(value, true) + }, + { + deep: true + } + ) + const setSchema = async (data: IPageSchema, pageId?: string) => { const newSchema = JSON.parse(JSON.stringify(data || schema)) reset(schema) @@ -71,9 +91,9 @@ export function useSchema( // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods setState(newSchema.state, true) - clearNodes() + await nextTick() - setPagecss(data.css, pageId) + setPageCss(data.css, pageId) Object.assign(schema, newSchema) // 当上下文环境设置完成之后再去处理区块属性访问器的watchEffect @@ -99,7 +119,6 @@ export function useSchema( } return { schema, - getSchema, setSchema, ...{ generateAccessor, @@ -109,14 +128,11 @@ export function useSchema( }, ...{ state, - getState, - setState, - deleteState + setState }, ...{ props, initProps, - getProps, setProps }, ...{ @@ -125,9 +141,8 @@ export function useSchema( setMethods }, ...{ - getContext, - getNode + getContext }, - setPagecss + setPageCss } } diff --git a/packages/canvas/render/src/page-block-function/state.ts b/packages/canvas/render/src/page-block-function/state.ts index d967b10512..8256b6ee45 100644 --- a/packages/canvas/render/src/page-block-function/state.ts +++ b/packages/canvas/render/src/page-block-function/state.ts @@ -1,20 +1,21 @@ import { shallowReactive } from 'vue' -import { reset } from '../data-utils' +import { getDeletedKeys } from '../data-utils' import { isStateAccessor, parseData } from '../data-function' export function useState(schema, { getContext, generateStateAccessors }) { const state = shallowReactive({}) - const getState = () => state - - const deleteState = (variable: string) => { - delete state[variable] - } const setState = (data, clear = false) => { - clear && reset(state) - if (!schema.state) { - schema.state = data + if (typeof data !== 'object' || data === null) { + return + } + + const deletedKeys = getDeletedKeys(state, data) + + // 同步删除的 key + for (const key of deletedKeys) { + delete state[key] } Object.assign(state, parseData(data, {}, getContext()) || {}) @@ -35,8 +36,6 @@ export function useState(schema, { getContext, generateStateAccessors }) { } return { state, - getState, - setState, - deleteState + setState } } diff --git a/packages/canvas/render/src/render.ts b/packages/canvas/render/src/render.ts index e6c0afbaff..d07426be3b 100644 --- a/packages/canvas/render/src/render.ts +++ b/packages/canvas/render/src/render.ts @@ -10,13 +10,14 @@ * */ -import { defineComponent, h, inject, provide, Ref } from 'vue' +import { defineComponent, h, inject, provide, Ref, Suspense } from 'vue' import { NODE_UID as DESIGN_UIDKEY, NODE_TAG as DESIGN_TAGKEY, NODE_LOOP as DESIGN_LOOPID } from '../../common' import { getDesignMode, DESIGN_MODE } from './canvas-function' import { parseCondition, parseData, parseLoopArgs } from './data-function' -import { blockSlotDataMap, getComponent, generateCollection, Mapper, configure } from './material-function' +import { blockSlotDataMap, getComponent, Mapper, configure } from './material-function' import { getPage } from './material-function/page-getter' +import BlockLoading from './BlockLoading.vue' export const renderDefault = (children, scope, parent) => children.map?.((child) => @@ -34,7 +35,7 @@ const stopEvent = (event) => { return false } -const generateSlotGroup = (children, isCustomElm, schema) => { +const generateSlotGroup = (children, schema) => { const slotGroup = {} children.forEach((child) => { @@ -42,7 +43,6 @@ const generateSlotGroup = (children, isCustomElm, schema) => { 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, @@ -55,9 +55,9 @@ const generateSlotGroup = (children, isCustomElm, schema) => { return slotGroup } -const renderSlot = (children, scope, schema, isCustomElm?) => { +const renderSlot = (children, scope, schema) => { if (children.some((a) => a.componentName === 'Template')) { - const slotGroup = generateSlotGroup(children, isCustomElm, schema) + const slotGroup = generateSlotGroup(children, schema) const slots = {} Object.keys(slotGroup).forEach((slotName) => { @@ -150,7 +150,7 @@ const injectPlaceHolder = (componentName, children) => { return children } -const renderGroup = (children, scope, parent, pageContext) => { +const renderGroup = (children, scope, pageContext) => { return children.map?.((schema) => { const { componentName, children, loop, loopArgs, condition, id } = schema const loopList = parseData(loop, scope, pageContext.context) @@ -163,8 +163,6 @@ const renderGroup = (children, scope, parent, pageContext) => { loopArgs }) - pageContext.setNode(schema, parent) - if (pageContext.conditions[id] === false || !parseCondition(condition, mergeScope, pageContext.context)) { return null } @@ -190,20 +188,22 @@ const getChildren = (schema, mergeScope, pageContext) => { const component = getComponent(componentName) const isNative = typeof component === 'string' - const isCustomElm = customElements[componentName] const isGroup = checkGroup(componentName) if (Array.isArray(renderChildren)) { - if (isNative || isCustomElm) { + // children 空的场景,不能返回空数组,因为有部分组件会误以为使用了自定义插槽,从而无法渲染默认插槽内容,比如 TinyTree 组件 + if (!renderChildren.length) { + return null + } + + if (isNative) { return renderDefault(renderChildren, mergeScope, schema) - } else { - return isGroup - ? renderGroup(renderChildren, mergeScope, schema, pageContext) - : renderSlot(renderChildren, mergeScope, schema, isCustomElm) } - } else { - return parseData(renderChildren, mergeScope, pageContext.context) + return isGroup + ? renderGroup(renderChildren, mergeScope, pageContext) + : renderSlot(renderChildren, mergeScope, schema) } + return parseData(renderChildren, mergeScope, pageContext.context) } function getRenderPageId(currentPageId, isPageStart) { const pagePathFromRoot = (inject('page-ancestors') as Ref).value @@ -239,8 +239,6 @@ export const renderer = defineComponent({ const { scope, schema, parent, ancestors } = this const { componentName, loop, loopArgs, condition } = schema const pageContext = this.currentPageContext - // 处理数据源和表格fetchData的映射关系 - generateCollection(schema) if (!componentName) { return parseData(schema, scope, pageContext.context) @@ -280,18 +278,28 @@ export const renderer = defineComponent({ mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData } - // 给每个节点设置schema.id,并缓存起来 - pageContext.setNode(schema, parent) - if (pageContext.conditions[schema.id] === false || !parseCondition(condition, mergeScope, pageContext.context)) { return null } - return h( + const Ele = h( component, getBindProps(schema, mergeScope, pageContext.context, pageContext), getChildren(schema, mergeScope, pageContext) ) + // 区块加上 suspense 渲染,就可以在网络延时的时候显示加载中的字样或者动画,优化体验 + if (schema.componentType === 'Block') { + return h( + Suspense, + {}, + { + default: () => Ele, + fallback: () => h(BlockLoading, { name: componentName }) + } + ) + } + + return Ele } return loopList?.length ? loopList.map(renderElement) : renderElement() diff --git a/packages/canvas/render/src/runner.ts b/packages/canvas/render/src/runner.ts index a1d033bfd5..25bf039435 100644 --- a/packages/canvas/render/src/runner.ts +++ b/packages/canvas/render/src/runner.ts @@ -15,7 +15,6 @@ import { addScript, addStyle, dynamicImportComponents, updateDependencies } from import TinyI18nHost, { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n' import Main, { api } from './RenderMain' import lowcode from './lowcode' -import { supportUmdBlock } from './supportUmdBlock' type ITinyI18nHostI18nHost = typeof TinyI18nHost interface IExtendsTinyI18nHost extends ITinyI18nHostI18nHost { @@ -33,8 +32,6 @@ const initRenderContext = () => { window.TinyLowcodeComponent = {} window.TinyComponentLibs = {} - supportUmdBlock() - document.addEventListener('updateDependencies', updateDependencies) } diff --git a/packages/canvas/render/src/supportUmdBlock.ts b/packages/canvas/render/src/supportUmdBlock.ts deleted file mode 100644 index b761223e10..0000000000 --- a/packages/canvas/render/src/supportUmdBlock.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as Vue from 'vue' -import * as VueI18n from 'vue-i18n' -import * as TinyWebcomponentCore from '@opentiny/tiny-engine-webcomponent-core' -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 './material-function' - -declare global { - interface Window { - Vue: typeof Vue - VueI18n: typeof VueI18n - TinyVue: typeof TinyVue - TinyVueIcon: typeof TinyVueIcon - TinyI18nHost: typeof TinyI18nHost - TinyWebcomponentCore: typeof TinyWebcomponentCore - } -} -// 和 @opentiny/tiny-engine-block-build 打包umd方式相适配 -export function supportUmdBlock() { - // 不能采用new Proxy代理Vue的方案,在编译后的vue会报错警告,采用一下方案扩展用于注入一些区块加载逻辑 - window.Vue = { - ...Vue, - resolveComponent(...args) { - // 此处先执行vue内部的解析组件的方法,如果可以拿到组件对象则直接返回,反之则去注册区块 - const component = Vue.resolveComponent(args[0]) - if (component && typeof component === 'string') { - return getComponent(capitalize(camelize(args[0]))) - } else { - return component - } - }, - // renderSlot方法第三个参数是作用域插槽传递的数据,格式{ data: vue.unref(state).componentData } - renderSlot(...args) { - // 获取当前vue的实例 - const instance = Vue.getCurrentInstance() - - // 获取当前区块名称 - const blockName = instance.attrs.dataTag as string - - const [, slotName, slotData] = args - - // 如果是作用域插槽,则获取作用域插槽传递过来的参数 - if (slotData) { - if (blockSlotDataMap[blockName]) { - blockSlotDataMap[blockName][slotName] = slotData - } else { - blockSlotDataMap[blockName] = { [slotName]: slotData } - } - } - - /** - * vue源码中的renderSlot会忽略default插槽的名称,所以这里必须手动添加args第三个参数的name值 - * vue源码如右所示:if (name !== 'default') props.name = name; return createVNode('slot', props, fallback && fallback()); - **/ - if (slotName === 'default') { - args[2] = args[2] || {} - args[2].name = slotName - } - - return Vue.renderSlot(...args) - } - } - - window.VueI18n = VueI18n - window.TinyVue = TinyVue - window.TinyVueIcon = TinyVueIcon - window.TinyWebcomponentCore = TinyWebcomponentCore - window.TinyI18nHost = TinyI18nHost -} diff --git a/packages/canvas/render/type.d.ts b/packages/canvas/render/type.d.ts index c4123ba76a..e146c2ba24 100644 --- a/packages/canvas/render/type.d.ts +++ b/packages/canvas/render/type.d.ts @@ -8,5 +8,7 @@ export declare global { scripts: Array } TinyGlobalConfig: Record + loadBlockComponent: (blockName: string) => Promise + host: any } } diff --git a/packages/common/component/BindI18n.vue b/packages/common/component/BindI18n.vue index c0f99ce0e3..eeb003de6f 100644 --- a/packages/common/component/BindI18n.vue +++ b/packages/common/component/BindI18n.vue @@ -4,31 +4,29 @@ - -
- {{ item.key }} - {{ item[currentLang] }} -
+
- +
国际化参数配置
- - - 创建新的多语言文案 - - 解除关联 +
+ 解除关联 + + 创建新的多语言文案 + +
@@ -43,9 +41,9 @@
-
- 国际化管理 - 添加并关联 +
+ 国际化管理 + 添加并关联
@@ -57,15 +55,13 @@ import { useLayout, useTranslate } from '@opentiny/tiny-engine-meta-register' import { PROP_DATA_TYPE } from '../js/constants' import { utils } from '@opentiny/tiny-engine-utils' import { Select, Option, Button, Input } from '@opentiny/vue' -import { iconPlus } from '@opentiny/vue-icon' export default { components: { TinySelect: Select, TinyOption: Option, TinyButton: Button, - TinyInput: Input, - IconPlus: iconPlus() + TinyInput: Input }, inheritAttrs: false, props: { @@ -169,10 +165,7 @@ export default { diff --git a/packages/common/component/BlockDeployDialog.vue b/packages/common/component/BlockDeployDialog.vue index 27b4dcaab8..dd2b911f70 100644 --- a/packages/common/component/BlockDeployDialog.vue +++ b/packages/common/component/BlockDeployDialog.vue @@ -1,6 +1,6 @@ diff --git a/packages/plugins/state/src/EditorI18nTool.vue b/packages/plugins/state/src/EditorI18nTool.vue index 28efda959b..757f3ac974 100644 --- a/packages/plugins/state/src/EditorI18nTool.vue +++ b/packages/plugins/state/src/EditorI18nTool.vue @@ -28,7 +28,7 @@ @@ -41,6 +41,8 @@ import { Button, Popover, Tooltip } from '@opentiny/vue' import { iconClose } from '@opentiny/vue-icon' import { BindI18n } from '@opentiny/tiny-engine-common' import { useTranslate } from '@opentiny/tiny-engine-meta-register' +import { constants } from '@opentiny/tiny-engine-utils' +const { OPEN_DELAY } = constants export default { components: { @@ -115,7 +117,8 @@ export default { onClosePopover, createI18n, handleChooseI18n, - handleConfirm + handleConfirm, + OPEN_DELAY } } } diff --git a/packages/plugins/state/src/Main.vue b/packages/plugins/state/src/Main.vue index 8d49ec06eb..94cd4ed340 100644 --- a/packages/plugins/state/src/Main.vue +++ b/packages/plugins/state/src/Main.vue @@ -22,10 +22,10 @@
- {{ activeName === STATE.CURRENT_STATE ? '添加变量' : '添加全局变量' }} + + + {{ activeName === STATE.CURRENT_STATE ? '添加变量' : '添加全局变量' }} +
{ - const { getSchema } = useCanvas().canvasApi.value + const { getSchema } = useCanvas() if (getSchema()) { if (updateKey.value !== name && flag.value === OPTION_TYPE.UPDATE) { @@ -193,7 +191,7 @@ export default { const confirm = () => { const { name } = state.createData - const { setState, setGlobalState } = useCanvas().canvasApi.value + const { getSchema, updateSchema } = useCanvas() if (!name || errorMessage.value) { notifySaveError('变量名未填写或名称不符合规范,请按照提示修改后重试。') @@ -216,8 +214,9 @@ export default { isPanelShow.value = false setSaved(false) - // 触发画布渲染 - setState({ [name]: variable }) + const schema = getSchema() + updateSchema({ state: { ...(schema.state || {}), [name]: variable } }) + useHistory().addHistory() } else { const validateResult = validateMonacoEditorData(storeRef.value.getEditor(), 'state字段', { required: true }) @@ -248,7 +247,7 @@ export default { const { id } = getMetaApi(META_SERVICE.GlobalService).getBaseInfo() updateGlobalState(id, { global_state: storeList }).then((res) => { isPanelShow.value = false - setGlobalState(res.global_state || []) + useResource().appSchemaState.globalState = res.global_state || [] }) } openCommon() @@ -263,11 +262,13 @@ export default { } const remove = (key) => { - const { deleteState, getSchema } = useCanvas().canvasApi.value + const { getSchema, updateSchema } = useCanvas() delete state.dataSource[key] - // 删除变量也需要同步触发画布渲染 - deleteState(key) + + const schema = getSchema() + let { lifeCycles } = schema + const { [key]: deletedKey, ...restState } = schema.state if (key.startsWith('datasource')) { const pageSchema = getSchema() @@ -280,9 +281,11 @@ export default { */ const pattern = new RegExp(`([\\s\\n]*\\/\\*\\* ${start} \\*\\/[\\s\\S]*\\/\\*\\* ${end} \\*\\/)`) - pageSchema.lifeCycles.setup.value = pageSchema.lifeCycles.setup.value.replace(pattern, '') + lifeCycles.setup.value = pageSchema.lifeCycles.setup.value.replace(pattern, '') } + updateSchema({ state: restState, lifeCycles }) + // 如果删除的是当前编辑的状态变量,则需要关闭二级面板 if (state.createData.name === key) { isPanelShow.value = false @@ -292,8 +295,7 @@ export default { } const setGlobalStateToDataSource = () => { - const { getGlobalState } = useCanvas().canvasApi.value - const globalState = getGlobalState() + const globalState = useResource().appSchemaState.globalState if (!globalState) { state.dataSource = {} @@ -301,20 +303,19 @@ export default { return } - state.dataSource = getGlobalState().reduce((acc, store) => ({ ...acc, [store.id]: store }), {}) + state.dataSource = globalState.reduce((acc, store) => ({ ...acc, [store.id]: store }), {}) } const removeStore = (key) => { - const storeListt = [...useResource().resState.globalState] || [] - const index = storeListt.findIndex((store) => store.id === key) - const { setGlobalState } = useCanvas().canvasApi.value + const storeList = [...useResource().appSchemaState.globalState] || [] + const index = storeList.findIndex((store) => store.id === key) if (index !== -1) { const { id } = getMetaApi(META_SERVICE.GlobalService).getBaseInfo() - storeListt.splice(index, 1) - updateGlobalState(id, { global_state: storeListt }).then((res) => { - setGlobalState(res.global_state) + storeList.splice(index, 1) + updateGlobalState(id, { global_state: storeList }).then((res) => { + useResource().appSchemaState.globalState = res.global_state || [] setGlobalStateToDataSource() }) @@ -330,15 +331,14 @@ export default { } const initDataSource = (tabsName = activeName.value) => { - const { getSchema } = useCanvas().canvasApi.value + const { getSchema } = useCanvas() if (tabsName === STATE.GLOBAL_STATE) { setGlobalStateToDataSource() } else { const pageSchema = getSchema() || {} - pageSchema.state = pageSchema?.state || {} - state.dataSource = pageSchema.state + state.dataSource = pageSchema.state || {} } } @@ -400,11 +400,20 @@ export default { width: 100%; .tiny-button { width: 100%; - border-color: var(--ti-lowcode-data-source-border-color); + border-color: var(--te-common-border-default); + &:hover { + border-color: var(--te-common-border-hover); + } } - .icon-plus { + .add-btn-icon { margin-right: 4px; + font-size: 16px; stroke: var(--ti-lowcode-chat-model-button-text); + color: var(--te-common-icon-secondary); + vertical-align: sub; + } + .add-btn-text { + display: inline-block; } } @@ -421,7 +430,7 @@ export default { } .left-filter { - margin-top: 12px; + margin-top: 4px; padding: 0 10px; } @@ -445,6 +454,7 @@ export default { height: 100%; border-right: 1px solid var(--ti-lowcode-toolbar-border-color); background: var(--ti-lowcode-common-component-bg); + box-shadow: 6px 0px 3px 0px var(--te-base-box-shadow-rgba-3); position: absolute; left: var(--base-left-panel-width); top: 0; @@ -457,7 +467,7 @@ export default { padding: 0 12px; font-size: 12px; font-weight: 700; - color: var(--ti-lowcode-data-source-color); + color: var(--te-common-text-primary); background: var(--ti-lowcode-common-component-bg); border-bottom: 1px solid var(--ti-lowcode-data-header-border-bottom-color); .options-wrap { diff --git a/packages/plugins/state/src/StateTips.vue b/packages/plugins/state/src/StateTips.vue index 8599ba91bf..41d010e5e4 100644 --- a/packages/plugins/state/src/StateTips.vue +++ b/packages/plugins/state/src/StateTips.vue @@ -13,12 +13,12 @@ diff --git a/packages/plugins/tree/package.json b/packages/plugins/tree/package.json index 18927a2ec8..a08bc95d2c 100644 --- a/packages/plugins/tree/package.json +++ b/packages/plugins/tree/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-plugin-tree", - "version": "2.0.0-rc.4", + "version": "2.1.0", "publishConfig": { "access": "public" }, diff --git a/packages/plugins/tree/src/Main.vue b/packages/plugins/tree/src/Main.vue index 6c87a84b4d..130cc688a0 100644 --- a/packages/plugins/tree/src/Main.vue +++ b/packages/plugins/tree/src/Main.vue @@ -29,7 +29,7 @@ @mouseleave="mouseleave(data.row)" @click="checkElement(data.row)" > - + @@ -49,12 +49,12 @@ + diff --git a/packages/vue-generator/test/testcases/sfc/accessor/schema.json b/packages/vue-generator/test/testcases/sfc/accessor/schema.json new file mode 100644 index 0000000000..27a6c664dd --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/accessor/schema.json @@ -0,0 +1,101 @@ +{ + "componentName": "Page", + "fileName": "Accessor", + "css": "", + "props": {}, + "children": [ + { + "componentName": "Text", + "props": { + "className": "page-header", + "text": "测试 state accessor 出码场景" + } + } + ], + "state": { + "firstName": "Opentiny", + "lastName": "TinyEngine", + "nullValue": { + "defaultValue": null, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.nullValue = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "numberValue": { + "defaultValue": 0, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.numberValue = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "emptyStr": { + "defaultValue": "", + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.emptyStr = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "strVal": { + "defaultValue": "i am str.", + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.strVal = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "trueVal": { + "defaultValue": true, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.trueVal = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "falseVal": { + "defaultValue": false, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.falseVal = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "arrVal": { + "defaultValue": [1, "2", { "aaa": "aaa" }, [3, 4], true, false], + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.arrVal = `${this.state.firstName} ${this.state.lastName}` }" + } + } + }, + "objVal": { + "defaultValue": { + "aaa": "aaa", + "arr": [1, "2", true, false, 0], + "ccc": { "bbb": "bbb" }, + "d": 32432, + "e": "", + "f": null, + "g": false + }, + "accessor": { + "getter": { + "type": "JSFunction", + "value": "function() { this.state.objVal = `${this.state.firstName} ${this.state.lastName}` }" + } + } + } + }, + "lifeCycles": {}, + "methods": {} +} diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/block.schema.json b/packages/vue-generator/test/testcases/sfc/slotParams/block.schema.json new file mode 100644 index 0000000000..df79707482 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/slotParams/block.schema.json @@ -0,0 +1,47 @@ +{ + "state": { + "personData": { + "name": "李华", + "age": "20", + "address": "china" + } + }, + "methods": {}, + "componentName": "Block", + "css": "", + "props": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "Text", + "id": "52535213", + "props": { + "text": "请开启插槽" + } + }, + { + "id": "42562b66", + "componentName": "div", + "props": {}, + "children": [ + { + "componentName": "Slot", + "id": "6153675d", + "props": { + "name": "test", + "params": [ + { + "name": "testData", + "value": { + "type": "JSExpression", + "value": "this.state.personData" + } + } + ] + } + } + ] + } + ], + "fileName": "BlockSlotParams" +} diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsBlock.vue b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsBlock.vue new file mode 100644 index 0000000000..d86979f72f --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsBlock.vue @@ -0,0 +1,25 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue new file mode 100644 index 0000000000..bfa219de2d --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/slotParams/expected/slotParamsPage.vue @@ -0,0 +1,27 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/page.schema.json b/packages/vue-generator/test/testcases/sfc/slotParams/page.schema.json new file mode 100644 index 0000000000..316f5d6f14 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/slotParams/page.schema.json @@ -0,0 +1,41 @@ +{ + "state": {}, + "methods": {}, + "componentName": "Page", + "css": "", + "props": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "BlockSlotParams", + "props": {}, + "id": "63623253", + "componentType": "Block", + "children": [ + { + "id": "54396632", + "componentName": "Template", + "props": { + "slot": { + "name": "test", + "params": ["testData"] + } + }, + "children": [ + { + "id": "5242615f", + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "testData.name" + } + } + } + ] + } + ] + } + ], + "fileName": "slotParamsTest" +} diff --git a/packages/vue-generator/test/testcases/sfc/slotParams/slotParams.test.js b/packages/vue-generator/test/testcases/sfc/slotParams/slotParams.test.js new file mode 100644 index 0000000000..9d7efa124a --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/slotParams/slotParams.test.js @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import blockSchema from './block.schema.json' +import pageSchema from './page.schema.json' +import { formatCode } from '@/utils/formatCode' + +test('should generate slot and pass testData params', async () => { + const res = genSFCWithDefaultPlugin(blockSchema, []) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/slotParamsBlock.vue') +}) + +test('should generate slot params', async () => { + const res = genSFCWithDefaultPlugin(pageSchema, []) + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/slotParamsPage.vue') +}) diff --git a/packages/webcomponent/package.json b/packages/webcomponent/package.json index feb146b7bb..2e25249ae7 100644 --- a/packages/webcomponent/package.json +++ b/packages/webcomponent/package.json @@ -1,6 +1,6 @@ { "name": "@opentiny/tiny-engine-webcomponent-core", - "version": "2.0.0-rc.4", + "version": "2.1.0", "publishConfig": { "access": "public" },