From f57c80a25bb9123b28a914144b92d18f1014f189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=95=B8?= Date: Mon, 24 Nov 2025 10:42:40 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ai-data-set/table-col.json | 276 +++++++++++++++++++++ docs/ai-data-set/table-empty.json | 213 ++++++++++++++++ docs/ai-data-set/table-pager.json | 343 +++++++++++++++++++++++++ docs/ai-data-set/table-slot.json | 399 ++++++++++++++++++++++++++++++ 4 files changed, 1231 insertions(+) create mode 100644 docs/ai-data-set/table-col.json create mode 100644 docs/ai-data-set/table-empty.json create mode 100644 docs/ai-data-set/table-pager.json create mode 100644 docs/ai-data-set/table-slot.json diff --git a/docs/ai-data-set/table-col.json b/docs/ai-data-set/table-col.json new file mode 100644 index 000000000..37bc305ee --- /dev/null +++ b/docs/ai-data-set/table-col.json @@ -0,0 +1,276 @@ +{ + "componentName": "Page", + "fileName": "TestTabel", + "css": ".page-base-style {\n margin: 0;\n padding: 0;\n height: 100%;\n background: #ffffff;\n}\n", + "props": { + "className": "page-base-style page-ytlek" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "index", + "width": 60 + }, + { + "type": "selection", + "width": "40" + }, + { + "title": "公司名称", + "width": "200", + "field": "name", + "showOverflow": "ellipsis" + }, + { + "field": "employees", + "title": "员工数", + "filter": false + }, + { + "field": "created_date", + "title": "创建日期", + "sortable": false + }, + { + "field": "city", + "title": "城市", + "filter": false + }, + { + "title": "启用", + "field": "boole", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "165b5af6", + "children": [ + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "row.boole", + "model": true + }, + "className": "" + }, + "children": [], + "id": "45426556" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "标签", + "field": "tags", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "45f34d75", + "children": [ + { + "componentName": "TinyTagGroup", + "props": { + "data": { + "type": "JSExpression", + "value": "this.getTagInfo(row.tags)" + }, + "className": "", + "size": "small" + }, + "children": [], + "id": "33244226" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "状态", + "field": "status", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "653da333", + "children": [ + { + "componentName": "TinyBadge", + "props": { + "data": "我的待办", + "value": { + "type": "JSExpression", + "value": "row.status" + }, + "max": 99, + "className": "", + "type": "success" + }, + "children": [], + "id": "2d421413" + } + ] + } + ], + "params": [ + "row" + ] + } + } + } + ], + "className": "", + "sortable": true, + "row-id": "id", + "data": [ + { + "id": "1", + "name": "GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false, + "status": 1, + "tags": [ + "科技", + "信息", + "测试" + ] + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "3", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "4", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "5", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "6", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "7", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + }, + { + "id": "8", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + } + ] + }, + "children": [], + "id": "63532263" + } + ], + "dataSource": { + "list": [] + }, + "methods": { + "getTagInfo": { + "type": "JSFunction", + "value": "function getTagInfo(tags) {\n return tags.map((e) => ({ name: e, type: 'success' }))\n}\n" + } + }, + "bridge": { + "imports": [] + }, + "state": { + "selectVal": "" + }, + "inputs": [], + "outputs": [], + "id": "body" +} \ No newline at end of file diff --git a/docs/ai-data-set/table-empty.json b/docs/ai-data-set/table-empty.json new file mode 100644 index 000000000..f18a3af07 --- /dev/null +++ b/docs/ai-data-set/table-empty.json @@ -0,0 +1,213 @@ +{ + "componentName": "Page", + "fileName": "TestTabel", + "css": ".page-base-style {\n margin: 0;\n padding: 0;\n height: 100%;\n background: #ffffff;\n}\n.page-ytlek {\n padding-top: 32px;\n padding-left: 12px;\n padding-right: 12px;\n}\n", + "props": { + "className": "page-base-style page-ytlek" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "index", + "width": 60 + }, + { + "type": "selection", + "width": "40" + }, + { + "title": "公司名称", + "width": "200", + "field": "name", + "showOverflow": "ellipsis" + }, + { + "field": "employees", + "title": "员工数", + "filter": false + }, + { + "field": "created_date", + "title": "创建日期", + "sortable": false, + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "62552322", + "children": [ + { + "componentName": "TinyDatePicker", + "props": { + "className": "", + "modelValue": { + "type": "JSExpression", + "value": "row.created_date", + "model": true + }, + "type": "date" + }, + "children": [], + "id": "75e6262e" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "field": "city", + "title": "城市", + "filter": false + }, + { + "title": "启用", + "field": "boole", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "165b5af6", + "children": [ + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "row.boole", + "model": true + }, + "className": "" + }, + "children": [], + "id": "45426556" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "标签", + "field": "tags", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "45f34d75", + "children": [ + { + "componentName": "TinyTagGroup", + "props": { + "data": { + "type": "JSExpression", + "value": "this.getTagInfo(row.tags)" + }, + "className": "", + "size": "small" + }, + "children": [], + "id": "33244226" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "状态", + "field": "status", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "653da333", + "children": [ + { + "componentName": "TinyBadge", + "props": { + "data": "我的待办", + "value": { + "type": "JSExpression", + "value": "row.status" + }, + "max": 99, + "className": "", + "type": "success" + }, + "children": [], + "id": "2d421413" + } + ] + } + ], + "params": [ + "row" + ] + } + } + } + ], + "className": "", + "sortable": false, + "row-id": "id", + "data": [], + "highlight-hover-row": false, + "resizable": true, + "size": "small", + "border": false, + "highlight-current-row": false, + "seq-serial": false + }, + "id": "63532263", + "children": [] + } + ], + "dataSource": { + "list": [] + }, + "methods": { + "getTagInfo": { + "type": "JSFunction", + "value": "function getTagInfo(tags) {\n return tags.map((e) => ({ name: e, type: 'success' }))\n}\n" + } + }, + "bridge": { + "imports": [] + }, + "state": { + "selectVal": "" + }, + "inputs": [], + "outputs": [], + "id": "body" +} \ No newline at end of file diff --git a/docs/ai-data-set/table-pager.json b/docs/ai-data-set/table-pager.json new file mode 100644 index 000000000..a0cb6a295 --- /dev/null +++ b/docs/ai-data-set/table-pager.json @@ -0,0 +1,343 @@ +{ + "componentName": "Page", + "fileName": "TestTabel", + "css": ".page-base-style {\n margin: 0;\n padding: 0;\n height: 100%;\n background: #ffffff;\n}\n", + "props": { + "className": "page-base-style page-ytlek" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "index", + "width": 60 + }, + { + "type": "selection", + "width": "40" + }, + { + "title": "公司名称", + "width": "200", + "field": "name", + "showOverflow": "ellipsis" + }, + { + "field": "employees", + "title": "员工数", + "filter": false + }, + { + "field": "created_date", + "title": "创建日期", + "sortable": false, + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "62552322", + "children": [ + { + "componentName": "TinyDatePicker", + "props": { + "className": "", + "modelValue": { + "type": "JSExpression", + "value": "row.created_date", + "model": true + }, + "type": "date" + }, + "children": [], + "id": "75e6262e" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "field": "city", + "title": "城市", + "filter": false + }, + { + "title": "启用", + "field": "boole", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "165b5af6", + "children": [ + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "row.boole", + "model": true + }, + "className": "" + }, + "children": [], + "id": "45426556" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "标签", + "field": "tags", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "45f34d75", + "children": [ + { + "componentName": "TinyTagGroup", + "props": { + "data": { + "type": "JSExpression", + "value": "this.getTagInfo(row.tags)" + }, + "className": "", + "size": "small" + }, + "children": [], + "id": "33244226" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "状态", + "field": "status", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "653da333", + "children": [ + { + "componentName": "TinyBadge", + "props": { + "data": "我的待办", + "value": { + "type": "JSExpression", + "value": "row.status" + }, + "max": 99, + "className": "", + "type": "success" + }, + "children": [], + "id": "2d421413" + } + ] + } + ], + "params": [ + "row" + ] + } + } + } + ], + "className": "", + "sortable": false, + "row-id": "id", + "data": [ + { + "id": "1", + "name": "GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false, + "status": 1, + "tags": [ + "科技", + "信息", + "测试" + ] + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "3", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "4", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "5", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "6", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "7", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + }, + { + "id": "8", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + } + ], + "highlight-hover-row": false, + "resizable": true, + "size": "small", + "border": false, + "highlight-current-row": false, + "seq-serial": false + }, + "id": "63532263", + "children": [ + { + "componentName": "Template", + "props": { + "slot": { + "name": "pager" + } + }, + "children": [ + { + "componentName": "TinyPager", + "props": { + "total": 50, + "page-sizes": [ + 5, + 7, + 10, + 20, + 50 + ], + "page-size": 10, + "mode": "number", + "layout": "total, sizes, prev, pager, next", + "className": "" + }, + "children": [], + "id": "33141f43" + } + ], + "id": "84346444" + } + ] + } + ], + "dataSource": { + "list": [] + }, + "methods": { + "getTagInfo": { + "type": "JSFunction", + "value": "function getTagInfo(tags) {\n return tags.map((e) => ({ name: e, type: 'success' }))\n}\n" + } + }, + "bridge": { + "imports": [] + }, + "state": { + "selectVal": "" + }, + "inputs": [], + "outputs": [], + "id": "body" +} \ No newline at end of file diff --git a/docs/ai-data-set/table-slot.json b/docs/ai-data-set/table-slot.json new file mode 100644 index 000000000..215f2bd40 --- /dev/null +++ b/docs/ai-data-set/table-slot.json @@ -0,0 +1,399 @@ +{ + "componentName": "Page", + "fileName": "TestTabel", + "css": ".page-base-style {\n margin: 0;\n padding: 0;\n height: 100%;\n background: #ffffff;\n}\n.page-ytlek {\n padding-top: 32px;\n padding-left: 12px;\n padding-right: 12px;\n}\n", + "props": { + "className": "page-base-style page-ytlek" + }, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyGrid", + "props": { + "editConfig": { + "trigger": "click", + "mode": "cell", + "showStatus": true + }, + "columns": [ + { + "type": "index", + "width": 60 + }, + { + "type": "selection", + "width": "40" + }, + { + "title": "公司名称", + "width": "200", + "field": "name", + "showOverflow": "ellipsis" + }, + { + "field": "employees", + "title": "员工数", + "filter": false + }, + { + "field": "created_date", + "title": "创建日期", + "sortable": false, + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "62552322", + "children": [ + { + "componentName": "TinyDatePicker", + "props": { + "className": "", + "modelValue": { + "type": "JSExpression", + "value": "row.created_date", + "model": true + }, + "type": "date" + }, + "children": [], + "id": "75e6262e" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "field": "city", + "title": "城市", + "filter": false + }, + { + "title": "启用", + "field": "boole", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "165b5af6", + "children": [ + { + "componentName": "TinySwitch", + "props": { + "modelValue": { + "type": "JSExpression", + "value": "row.boole", + "model": true + }, + "className": "" + }, + "children": [], + "id": "45426556" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "标签", + "field": "tags", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "45f34d75", + "children": [ + { + "componentName": "TinyTagGroup", + "props": { + "data": { + "type": "JSExpression", + "value": "this.getTagInfo(row.tags)" + }, + "className": "", + "size": "small" + }, + "children": [], + "id": "33244226" + } + ] + } + ], + "params": [ + "row" + ] + } + } + }, + { + "title": "状态", + "field": "status", + "slots": { + "default": { + "type": "JSSlot", + "value": [ + { + "componentName": "div", + "id": "653da333", + "children": [ + { + "componentName": "TinyBadge", + "props": { + "data": "我的待办", + "value": { + "type": "JSExpression", + "value": "row.status" + }, + "max": 99, + "className": "", + "type": "success" + }, + "children": [], + "id": "2d421413" + } + ] + } + ], + "params": [ + "row" + ] + } + } + } + ], + "className": "", + "sortable": false, + "row-id": "id", + "data": [ + { + "id": "1", + "name": "GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司", + "city": "福州", + "employees": 800, + "created_date": "2014-04-30 00:56:00", + "boole": false, + "status": 1, + "tags": [ + "科技", + "信息", + "测试" + ] + }, + { + "id": "2", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "3", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "4", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": false, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "5", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 2, + "tags": [ + "科技" + ] + }, + { + "id": "6", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 3, + "tags": [ + "科技" + ] + }, + { + "id": "7", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + }, + { + "id": "8", + "name": "WWW科技有限公司", + "city": "深圳", + "employees": 300, + "created_date": "2016-07-08 12:36:22", + "boole": true, + "status": 1, + "tags": [ + "科技" + ] + } + ], + "highlight-hover-row": false, + "resizable": true, + "size": "small", + "border": false, + "highlight-current-row": false, + "seq-serial": false + }, + "id": "63532263", + "children": [ + { + "componentName": "Template", + "props": { + "slot": { + "name": "toolbar" + } + }, + "children": [ + { + "componentName": "TinyForm", + "props": { + "labelWidth": "80px", + "labelPosition": "top", + "validate-type": "text", + "className": "", + "inline": true, + "label-position": "left" + }, + "children": [ + { + "componentName": "TinyFormItem", + "props": { + "label": "名称" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "" + }, + "id": "34428513" + } + ], + "id": "3463df55" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "标签" + }, + "children": [ + { + "componentName": "TinyInput", + "props": { + "placeholder": "请输入", + "modelValue": "", + "type": "password" + }, + "id": "aec38135" + } + ], + "id": "23a64112" + }, + { + "componentName": "TinyFormItem", + "props": { + "label": "" + }, + "children": [ + { + "componentName": "TinyButton", + "props": { + "text": "查询", + "type": "primary", + "style": "margin-right: 10px" + }, + "id": "14265332" + }, + { + "componentName": "TinyButton", + "props": { + "text": "重置", + "type": "default" + }, + "id": "2132d533" + } + ], + "id": "32d28245" + } + ], + "id": "41323245" + } + ], + "id": "32616241" + } + ] + } + ], + "dataSource": { + "list": [] + }, + "methods": { + "getTagInfo": { + "type": "JSFunction", + "value": "function getTagInfo(tags) {\n return tags.map((e) => ({ name: e, type: 'success' }))\n}\n" + } + }, + "bridge": { + "imports": [] + }, + "state": { + "selectVal": "" + }, + "inputs": [], + "outputs": [], + "id": "body" +} \ No newline at end of file From 2fd388099c50be4ad38f0ba1f2ef109c38d5f304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=95=B8?= Date: Mon, 24 Nov 2025 15:49:52 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0AI=E8=AE=AD?= =?UTF-8?q?=E7=BB=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ai-data-set/iconfont.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/ai-data-set/iconfont.json diff --git a/docs/ai-data-set/iconfont.json b/docs/ai-data-set/iconfont.json new file mode 100644 index 000000000..bb7ea78b4 --- /dev/null +++ b/docs/ai-data-set/iconfont.json @@ -0,0 +1 @@ +["audio-monitoring","phone-monitoring ","remark","vertical-line","wechat-2-fill","wechat-pay-line","wechat-fill","wechat-pay-fill","wechat-2-line","qq-fill","qq-line","send-plane-2-line","git-repository-fill","red-packet-fill","chat-1-fill","git-repository-line","refund-line","chat-1-line","git-repository-private-line","file-ppt-2-fill","price-tag-3-line","chat-2-line","git-repository-private-fill","file-ppt-2-line","chat-2-fill","file-ppt-fill","price-tag-2-fill","chat-3-fill","file-pdf-fill","chat-3-line","file-ppt-line","chat-check-fill","parentheses-line","file-reduce-line","chat-check-line","file-pdf-line","chat-4-fill","terminal-box-line","shopping-bag-line","chat-4-line","terminal-line","file-reduce-fill","chat-delete-fill","terminal-window-fill","chat-download-fill","terminal-box-fill","file-shield-2-fill","chat-download-line","terminal-window-line","file-settings-fill","price-tag-line","chat-delete-line","airplay-line","file-settings-line","file-shield-fill","shopping-basket-2-fill","file-shield-2-line","shopping-basket-2-line","chat-forward-line","chat-forward-fill","file-shield-line","chat-heart-fill","airplay-fill","file-text-fill","trophy-fill","chat-new-fill","battery-2-charge-fill","file-transfer-fill","swap-fill","chat-history-fill","battery-2-charge-line","file-text-line","ticket-2-fill","chat-history-line","battery-2-fill","file-unknow-fill","ticket-2-line","chat-new-line","battery-charge-fill","file-unknow-line","swap-line","chat-off-line","battery-charge-line","file-upload-fill","chat-heart-line","battery-fill","file-user-fill","swap-box-fill","chat-private-line","base-station-line","file-user-line","trophy-line","chat-off-fill","battery-2-line","file-warning-fill","water-flash-fill","chat-private-fill","base-station-fill","file-word-2-fill","water-flash-line","chat-poll-fill","battery-low-fill","file-word-fill","heart-add-fill","chat-poll-line","battery-low-line","file-word-2-line","heart-2-fill","chat-settings-line","battery-saver-fill","file-word-line","heart-add-line","chat-smile-3-line","battery-saver-line","file-zip-fill","heart-2-line","chat-smile-line","battery-share-line","file-zip-line","apple-fill","chat-smile-fill","battery-share-fill","folder-2-fill","app-store-fill","chat-quote-line","bluetooth-fill","file-transfer-line","app-store-line","chat-upload-fill","bluetooth-line","folder-3-fill","apple-line","chat-smile-3-fill","bluetooth-connect-line","folder-4-fill","centos-line","chat-settings-fill","cast-fill","folder-3-line","centos-fill","chat-smile-2-fill","cast-line","folder-5-fill","open-source-fill","chat-upload-line","bluetooth-connect-fill","folder-5-line","open-source-line","chat-quote-fill","cellphone-line","folder-add-fill","bus-line","chat-smile-2-line","computer-fill","file-warning-line","barricade-line","feedback-fill","computer-line","folder-add-line","bike-fill","message-3-fill","cpu-fill","file-shred-line","barricade-fill","message-2-fill","cpu-line","folder-chart-2-line","bus-2-fill","discuss-line","cellphone-fill","folder-chart-2-fill","bike-line","message-2-line","dashboard-2-fill","folder-chart-fill","bus-2-line","message-3-line","dashboard-2-line","folder-chart-line","bus-fill","chat-voice-fill","dashboard-3-fill","folder-download-fill","bus-wifi-line","feedback-line","database-fill","folder-fill","bus-wifi-fill","message-fill","database-2-line","folder-4-line","compass-2-line","message-line","database-2-fill","folder-download-line","car-fill","discuss-fill","dashboard-3-line","folder-forbid-fill","car-washing-line","ancient-gate-fill","question-answer-fill","device-fill","folder-forbid-line","caravan-line","ancient-gate-line","question-answer-line","database-line","folder-2-line","compass-2-fill","chat-voice-line","device-recover-line","folder-history-fill","caravan-fill","building-2-line","questionnaire-fill","device-line","folder-history-line","charging-pile-line","video-chat-fill","device-recover-fill","folder-info-fill","car-line","building-2-fill","video-chat-line","dual-sim-1-line","folder-keyhole-line","charging-pile-2-fill","building-4-line","questionnaire-line","dual-sim-1-fill","folder-info-line","china-railway-fill","anticlockwise-2-line","dual-sim-2-line","folder-keyhole-fill","car-washing-fill","government-line","anticlockwise-fill","dual-sim-2-fill","folder-lock-fill","china-railway-line","government-fill","anticlockwise-2-fill","fingerprint-2-fill","folder-line","compass-3-fill","anticlockwise-line","gamepad-fill","folder-music-fill","charging-pile-2-line","artboard-2-line","fingerprint-line","folder-lock-line","compass-line","home-3-line","artboard-2-fill","gradienter-fill","folder-open-fill","compass-fill","home-4-line","artboard-line","fingerprint-fill","folder-open-line","compass-4-fill","home-3-fill","ball-pen-line","gps-fill","folder-received-line","e-bike-2-fill","home-2-fill","ball-pen-fill","gps-line","folder-reduce-fill","compass-discover-fill","home-5-line","blur-off-line","hard-drive-2-fill","folder-received-fill","compass-4-line","home-4-fill","blur-off-fill","hard-drive-2-line","folder-reduce-line","direction-fill","home-5-fill","artboard-fill","gradienter-line","folder-shared-fill","e-bike-line","home-6-line","brush-3-fill","gamepad-line","folder-music-line","cup-line","home-6-fill","brush-4-fill","hard-drive-fill","folder-settings-fill","cup-fill","home-7-fill","brush-2-fill","hotspot-fill","folder-shield-2-fill","direction-line","home-7-line","brush-2-line","fingerprint-2-line","folder-shield-2-line","e-bike-2-line","home-8-line","brush-3-line","hotspot-line","folder-transfer-fill","compass-3-line","building-4-fill","clockwise-2-line","install-line","folder-shield-line","e-bike-fill","home-8-fill","brush-fill","install-fill","folder-shield-fill","compass-discover-line","clockwise-2-fill","hard-drive-line","folder-shared-line","charging-pile-fill","home-heart-fill","clockwise-line","keyboard-box-fill","folder-unknow-line","flight-takeoff-fill","home-heart-line","collage-fill","keyboard-line","folder-unknow-fill","flight-takeoff-line","home-gear-fill","clockwise-fill","mac-fill","folder-transfer-line","flight-land-line","home-smile-fill","brush-4-line","mac-line","folder-upload-fill","footprint-fill","home-smile-line","collage-line","macbook-line","folder-user-fill","globe-fill","home-smile-2-line","brush-line","mouse-fill","folder-settings-line","home-smile-2-fill","compasses-2-fill","keyboard-fill","folder-user-line","gas-station-line","hospital-line","compasses-2-line","phone-find-fill","folder-warning-fill","gas-station-fill","hospital-fill","compasses-fill","mouse-line","folder-zip-fill","home-wifi-line","compasses-line","phone-find-line","folder-zip-line","guide-fill","hotel-line","contrast-2-fill","phone-lock-line","folders-fill","globe-line","store-2-line","contrast-2-line","keynote-fill","flight-land-fill","store-2-fill","contrast-drop-2-fill","folder-warning-line","store-3-line","contrast-fill","phone-line","keynote-line","hotel-fill","contrast-drop-fill","qr-scan-2-line","markdown-fill","footprint-line","home-gear-line","contrast-drop-2-line","qr-scan-line","folder-upload-line","map-fill","store-line","contrast-drop-line","qr-scan-fill","newspaper-fill","luggage-deposit-line","store-3-fill","contrast-line","macbook-fill","newspaper-line","map-pin-2-fill","store-fill","crop-2-fill","phone-fill","numbers-fill","luggage-cart-line","home-wifi-fill","crop-fill","remote-control-2-fill","markdown-line","map-line","crop-line","radar-line","pages-line","hotel-bed-line","advertisement-line","crop-2-line","battery-line","pages-fill","map-pin-3-line","advertisement-fill","drag-move-2-fill","radar-fill","folders-line","archive-line","drag-drop-fill","remote-control-2-line","sticky-note-2-fill","drag-move-fill","qr-scan-2-fill","numbers-line","archive-fill","drag-move-line","remote-control-line","sticky-note-fill","at-fill","drop-fill","sticky-note-2-line","map-pin-3-fill","at-line","drop-line","remote-control-fill","sticky-note-line","luggage-deposit-fill","attachment-fill","drag-move-2-line","rotate-lock-line","survey-fill","map-pin-user-fill","award-fill","drag-drop-line","router-line","task-line","map-pin-add-fill","award-line","edit-box-fill","router-fill","survey-line","map-pin-2-line","bar-chart-2-line","edit-box-line","phone-lock-fill","task-fill","map-pin-user-line","bar-chart-fill","edit-circle-fill","rss-line","todo-fill","map-pin-5-fill","bar-chart-box-line","edit-circle-line","save-2-line","file-upload-line","map-pin-5-line","bar-chart-grouped-fill","edit-fill","rotate-lock-fill","todo-line","bar-chart-2-fill","rss-fill","align-bottom","bar-chart-box-fill","edit-line","save-2-fill","align-justify","bar-chart-grouped-line","focus-2-fill","save-fill","align-right","bar-chart-horizontal-fill","save-3-line","a-b","bar-chart-horizontal-line","eraser-fill","save-line","align-left","bar-chart-line","focus-fill","save-3-fill","align-center","bookmark-2-fill","eraser-line","scan-fill","asterisk","bookmark-2-line","focus-3-fill","scan-line","align-vertically","focus-2-line","scan-2-line","attachment-2","bookmark-fill","grid-line","scan-2-fill","bold","bookmark-line","hammer-fill","sd-card-fill","bring-to-front","focus-3-line","sd-card-mini-fill","bring-forward","briefcase-2-fill","hammer-line","sd-card-mini-line","briefcase-2-line","focus-line","sensor-line","delete-column","briefcase-3-line","ink-bottle-line","server-line","delete-row","input-method-line","server-fill","align-top","briefcase-4-fill","input-method-fill","sensor-fill","double-quotes-l","briefcase-5-fill","grid-fill","sd-card-line","double-quotes-r","briefcase-fill","layout-3-line","shut-down-fill","flow-chart","briefcase-5-line","layout-3-fill","shut-down-line","emphasis-cn","bubble-chart-fill","layout-2-line","signal-wifi-1-fill","emphasis","bubble-chart-line","layout-4-line","signal-wifi-2-fill","font-color","calculator-line","layout-2-fill","signal-wifi-1-line","english-input","calculator-fill","layout-4-fill","signal-wifi-3-line","font-size-2","calendar-2-fill","layout-5-line","signal-wifi-3-fill","font-size","briefcase-4-line","layout-5-fill","signal-wifi-error-fill","format-clear","briefcase-line","layout-6-fill","signal-wifi-2-line","functions","calendar-2-line","layout-bottom-2-fill","signal-wifi-error-line","h-1","calendar-check-fill","layout-6-line","signal-wifi-fill","h-3","calendar-check-line","layout-bottom-2-line","signal-wifi-line","h-2","calendar-event-fill","layout-bottom-fill","signal-wifi-off-fill","h-4","calendar-event-line","layout-column-fill","sim-card-2-fill","h-6","calendar-line","layout-bottom-line","signal-wifi-off-line","heading","calendar-fill","layout-column-line","sim-card-2-line","h-5","layout-left-2-fill","sim-card-fill","hashtag","layout-fill","smartphone-fill","indent-increase","cloud-fill","layout-grid-fill","smartphone-line","input-cursor-move","attachment-line","layout-grid-line","tablet-fill","indent-decrease","cloud-line","layout-left-2-line","sim-card-line","insert-column-left","cloud-off-fill","layout-left-fill","tablet-line","insert-column-right","cloud-off-line","layout-left-line","tv-2-fill","insert-row-bottom","customer-service-2-line","ink-bottle-fill","tv-fill","italic","customer-service-fill","layout-line","tv-2-line","line-height","flag-2-fill","layout-right-2-line","u-disk-fill","link-m","customer-service-line","layout-right-fill","u-disk-line","link-unlink-m","layout-right-2-fill","tv-line","link-unlink","flag-2-line","layout-right-line","uninstall-fill","link","customer-service-2-fill","layout-row-fill","uninstall-line","list-check","donut-chart-line","layout-masonry-fill","usb-fill","list-unordered","donut-chart-fill","layout-masonry-line","usb-line","merge-cells-horizontal","layout-row-line","wifi-line","list-ordered","global-fill","layout-top-2-fill","wifi-off-line","merge-cells-vertical","honour-line","layout-top-fill","wifi-off-fill","insert-row-top","honour-fill","layout-top-2-line","wifi-fill","node-tree","inbox-archive-line","layout-top-line","wireless-charging-fill","mind-map","global-line","magic-fill","wireless-charging-line","list-check-2","inbox-archive-fill","mark-pen-fill","organization-chart","inbox-line","magic-line","page-separator","inbox-unarchive-fill","markup-fill","bill-fill","question-mark","inbox-unarchive-line","mark-pen-line","book-2-fill","send-backward","line-chart-line","markup-line","book-3-fill","rounded-corner","line-chart-fill","paint-brush-fill","bill-line","pinyin-input","inbox-fill","paint-line","book-fill","send-to-back","paint-fill","book-2-line","paragraph","mail-check-fill","palette-fill","book-line","omega","links-fill","palette-line","book-mark-fill","separator","links-line","pantone-fill","book-open-fill","single-quotes-l","mail-check-line","paint-brush-line","book-open-line","single-quotes-r","pantone-line","book-read-fill","sort-asc","mail-close-line","pen-nib-line","booklet-line","sort-desc","mail-download-line","pen-nib-fill","booklet-fill","space","mail-close-fill","pencil-fill","book-3-line","split-cells-vertical","mail-fill","pencil-ruler-fill","clipboard-fill","split-cells-horizontal","mail-forbid-fill","pencil-line","contacts-book-2-fill","strikethrough-2","mail-line","pencil-ruler-line","book-read-line","superscript-2","mail-forbid-line","quill-pen-fill","contacts-book-2-line","strikethrough","mail-lock-fill","ruler-2-fill","contacts-book-fill","subscript","mail-download-fill","quill-pen-line","contacts-book-line","subscript-2","mail-lock-line","pencil-ruler-2-fill","contacts-book-upload-fill","superscript","mail-open-fill","ruler-fill","book-mark-line","table-2","mail-open-line","ruler-2-line","contacts-book-upload-line","text-direction-l","mail-send-line","pencil-ruler-2-line","draft-fill","text-direction-r","mail-send-fill","scissors-2-fill","clipboard-line","text-wrap","mail-settings-line","scissors-2-line","draft-line","text-spacing","mail-star-line","scissors-cut-fill","file-2-line","text","mail-star-fill","ruler-line","file-2-fill","translate","mail-unread-line","scissors-fill","file-3-fill","underline","mail-unread-fill","scissors-line","file-3-line","wubi-input","mail-volume-line","screenshot-2-fill","file-4-fill","translate-2","medal-2-fill","screenshot-2-line","file-4-line","auction-fill","medal-line","shape-2-fill","coin-fill","medal-2-line","screenshot-line","file-chart-2-line","bank-card-2-fill","pie-chart-2-fill","screenshot-fill","bank-card-line","pie-chart-2-line","shape-fill","file-chart-2-fill","bank-card-fill","pie-chart-box-line","shape-line","file-chart-line","coin-line","medal-fill","sip-line","file-chart-fill","bank-card-2-line","mail-settings-fill","shape-2-line","file-cloud-fill","auction-line","pie-chart-fill","slice-fill","file-code-fill","24-hours-fill","mail-volume-fill","slice-line","file-cloud-line","coupon-2-fill","pie-chart-line","t-box-line","file-code-line","coupon-4-fill","printer-fill","t-box-fill","file-copy-2-fill","coupon-5-fill","pie-chart-box-fill","table-alt-line","file-copy-2-line","coupon-2-line","printer-cloud-fill","table-fill","file-copy-fill","printer-cloud-line","table-alt-fill","file-copy-line","coupon-4-line","printer-line","table-line","file-damage-fill","copper-diamond-line","tools-fill","coupon-3-line","tools-line","coupon-3-fill","projector-2-fill","sip-fill","file-excel-2-fill","coupon-fill","projector-2-line","brackets-fill","file-excel-fill","exchange-box-fill","projector-line","braces-fill","file-excel-2-line","exchange-funds-line","record-mail-fill","braces-line","file-forbid-fill","exchange-funds-fill","projector-fill","brackets-line","file-excel-line","coupon-5-line","record-mail-line","code-box-fill","file-damage-line","coupon-line","registered-fill","file-edit-line","exchange-fill","file-forbid-line","exchange-box-line","reply-all-fill","bug-line","file-fill","funds-box-fill","file-gif-fill","exchange-line","code-box-line","file-gif-line","24-hours-line","reply-all-line","file-history-line","hand-heart-fill","send-plane-2-fill","code-line","file-edit-fill","gift-fill","send-plane-fill","bug-fill","file-hwp-line","funds-fill","service-fill","code-s-line","file-history-fill","money-cny-box-line","send-plane-line","code-s-slash-line","slideshow-2-line","file-line","hand-coin-fill","slideshow-3-fill","command-fill","file-info-line","increase-decrease-fill","slideshow-2-fill","file-list-2-fill","increase-decrease-line","slideshow-4-line","file-info-fill","gift-2-fill","service-line","cursor-fill","gift-2-line","slideshow-3-line","funds-line","slideshow-4-fill","git-branch-fill","file-list-2-line","funds-box-line","stack-line","git-branch-line","file-list-fill","hand-heart-line","slideshow-fill","git-commit-fill","file-lock-fill","hand-coin-line","slideshow-line","cursor-line","file-list-line","percent-line","trademark-fill","git-merge-fill","file-mark-fill","safe-fill","stack-fill","git-commit-line","file-music-fill","price-tag-fill","trademark-line","git-merge-line","file-lock-line","refund-fill","window-2-line","git-pull-request-line","file-mark-line","safe-line","window-2-fill","git-pull-request-fill","file-music-line","window-fill","git-repository-commits-line","file-paper-2-fill","price-tag-2-line","window-line","git-repository-commits-fill","file-paper-2-line","price-tag-3-fill","map-pin-4-line","navigation-line","map-pin-4-fill","map-pin-time-fill","navigation-fill","map-pin-time-line","map-pin-add-line","motorbike-fill","motorbike-line","guide-line","pin-distance-fill","oil-fill","parking-box-line","parking-box-fill","plane-fill","parking-line","passport-fill","police-car-fill","pushpin-fill","police-car-line","pin-distance-line","oil-line","pushpin-2-line","parking-fill","pushpin-2-fill","passport-line","road-map-fill","roadster-fill","roadster-line","restaurant-2-fill","rocket-2-fill","restaurant-fill","rocket-2-line","restaurant-line","road-map-line","pushpin-line","rocket-fill","riding-fill","restaurant-2-line","riding-line","rocket-line","run-fill","ship-fill","signal-tower-fill","signal-tower-line","sailboat-line","steering-2-line","ship-2-fill","space-ship-fill","space-ship-line","ship-line","steering-2-fill","ship-2-line","sailboat-fill","suitcase-2-fill","suitcase-3-fill","suitcase-2-line","takeaway-line","steering-line","suitcase-3-line","takeaway-fill","steering-fill","subway-wifi-line","suitcase-line","subway-wifi-fill","suitcase-fill","truck-line","taxi-wifi-fill","taxi-fill","subway-fill","train-line","treasure-map-fill","taxi-line","train-fill","walk-fill","truck-fill","train-wifi-fill","train-wifi-line","taxi-wifi-line","speaker-3-line","user-6-line","speaker-line","eye-2-fill","user-line","user-location-fill","speed-mini-fill","filter-3-fill","user-location-line","speed-mini-line","filter-3-line","arrow-right-circle-line","stop-circle-fill","arrow-up-s-line","user-received-line","stop-circle-line","user-received-fill","stop-fill","user-search-fill","stop-line","filter-off-fill","user-search-line","stop-mini-line","filter-off-line","stop-mini-fill","find-replace-line","surround-sound-fill","find-replace-fill","user-settings-line","surround-sound-line","forbid-2-fill","user-shared-fill","tape-fill","forbid-fill","user-settings-fill","tape-line","user-shared-line","video-add-line","forbid-line","user-smile-fill","video-download-fill","user-smile-line","video-add-fill","history-line","user-star-fill","video-download-line","history-fill","user-star-line","video-fill","indeterminate-circle-fill","user-unfollow-fill","video-line","indeterminate-circle-line","open-arm-line","video-upload-fill","user-unfollow-line","video-upload-line","list-settings-line","vidicon-2-fill","list-settings-fill","women-fill","vidicon-2-line","loader-2-line","vidicon-fill","loader-3-line","vidicon-line","blaze-fill","voiceprint-fill","loader-4-line","blaze-line","cloud-windy-fill","volume-down-line","loader-5-line","cloud-windy-line","volume-down-fill","celsius-fill","cloudy-2-fill","volume-off-vibrate-line","cloudy-2-line","volume-off-vibrate-fill","lock-fill","cloudy-fill","lock-password-fill","earthquake-fill","lock-line","fire-fill","volume-vibrate-line","drizzle-fill","volume-vibrate-fill","cloudy-line","webcam-fill","lock-unlock-line","drizzle-line","webcam-line","lock-password-line","earthquake-line","login-box-fill","login-circle-line","fire-line","login-circle-fill","foggy-fill","logout-box-fill","hail-line","lock-unlock-fill","foggy-line","logout-box-line","hail-fill","flashlight-fill","flood-line","logout-circle-line","flood-fill","cake-2-fill","logout-circle-fill","haze-2-fill","cake-line","logout-circle-r-fill","flashlight-line","logout-circle-r-line","haze-fill","menu-2-fill","haze-line","menu-2-line","heavy-showers-line","door-fill","login-box-line","cake-fill","menu-3-fill","heavy-showers-fill","door-closed-fill","menu-3-line","mist-fill","door-closed-line","menu-4-fill","moon-clear-fill","door-line","menu-5-fill","moon-clear-line","door-lock-line","menu-4-line","mist-line","cake-2-line","menu-5-line","moon-cloudy-fill","haze-2-line","door-open-fill","moon-foggy-fill","door-lock-box-fill","menu-fill","moon-cloudy-line","moon-fill","moon-foggy-line","door-open-line","rainy-fill","menu-line","rainy-line","rainbow-fill","handbag-fill","rainbow-line","door-lock-box-line","more-2-fill","showers-fill","forbid-2-line","showers-line","key-2-fill","more-2-line","moon-line","more-fill","snowy-line","key-2-line","notification-badge-fill","sun-cloudy-fill","knife-fill","notification-badge-line","snowy-fill","leaf-line","sun-cloudy-line","leaf-fill","sun-foggy-fill","lightbulb-fill","radio-button-fill","sun-foggy-line","refresh-fill","sun-fill","lightbulb-flash-line","radio-button-line","sun-line","search-2-fill","temp-cold-line","outlet-2-fill","search-eye-fill","temp-hot-line","knife-line","search-eye-line","temp-hot-fill","lightbulb-line","thunderstorms-line","lightbulb-flash-fill","settings-2-line","tornado-fill","handbag-line","settings-2-fill","tornado-line","outlet-fill","search-2-line","typhoon-line","outlet-2-line","windy-fill","outlet-line","settings-3-line","temp-cold-fill","plant-fill","windy-line","typhoon-fill","plant-line","settings-3-fill","thunderstorms-fill","settings-6-fill","settings-5-fill","settings-fill","plug-fill","settings-6-line","plug-line","share-box-line","subway-line","settings-5-line","treasure-map-line","reserved-line","settings-line","walk-line","reserved-fill","share-box-fill","share-fill","scales-3-line","share-circle-line","4k-line","scales-fill","album-fill","share-circle-fill","4k-fill","share-forward-box-fill","album-line","shirt-fill","share-forward-box-line","scales-line","broadcast-fill","camera-2-fill","door-lock-fill","share-line","broadcast-line","shield-check-fill","camera-3-fill","shirt-line","shield-check-line","camera-2-line","shield-cross-fill","camera-3-line","shield-fill","camera-lens-fill","shield-cross-line","camera-switch-fill","shield-flash-fill","camera-lens-line","shield-flash-line","camera-fill","shield-keyhole-fill","camera-line","umbrella-fill","shield-keyhole-line","camera-off-fill","umbrella-line","more-line","camera-off-line","shield-line","clapperboard-fill","camera-switch-line","shield-user-fill","clapperboard-line","shield-user-line","disc-line","spam-3-line","spam-2-fill","scales-3-fill","star-half-s-line","disc-fill","star-s-fill","dvd-fill","add-circle-fill","spam-fill","dvd-line","add-box-fill","star-s-line","eject-fill","add-box-line","star-fill","eject-line","spam-2-line","star-half-fill","share-forward-2-line","side-bar-fill","alarm-warning-fill","thumb-up-line","add-circle-line","side-bar-line","alert-line","star-half-line","alarm-warning-line","star-half-s-fill","alert-fill","star-line","gallery-fill","apps-2-line","thumb-down-fill","apps-2-fill","thumb-up-fill","gallery-line","apps-fill","subtract-fill","gallery-upload-fill","arrow-down-fill","spam-line","hd-fill","arrow-down-line","thumb-down-line","gallery-upload-line","arrow-down-circle-fill","arrow-down-s-fill","timer-2-fill","image-2-fill","arrow-down-circle-line","arrow-down-s-line","spam-3-fill","hd-line","apps-line","timer-flash-fill","hq-fill","timer-fill","hq-line","timer-2-line","image-edit-fill","image-add-fill","upload-2-fill","image-2-line","toggle-fill","image-add-line","timer-flash-line","arrow-go-back-fill","image-edit-line","arrow-go-back-line","upload-cloud-fill","landscape-fill","arrow-left-circle-fill","landscape-line","arrow-go-forward-fill","timer-line","arrow-go-forward-line","upload-cloud-line","mic-2-fill","arrow-left-circle-line","live-line","arrow-left-down-fill","live-fill","arrow-left-fill","arrow-left-line","movie-line","arrow-left-down-line","movie-fill","arrow-left-right-fill","arrow-left-right-line","account-box-fill","arrow-left-s-fill","account-box-line","music-2-line","arrow-left-s-line","account-circle-fill","music-fill","arrow-left-up-fill","account-circle-line","music-line","arrow-left-up-line","account-pin-box-fill","mv-fill","arrow-right-circle-fill","notification-2-fill","arrow-right-down-line","aliens-fill","notification-2-line","arrow-right-fill","arrow-right-down-fill","account-pin-circle-line","notification-4-fill","arrow-right-line","account-pin-circle-fill","notification-4-line","arrow-right-up-fill","account-pin-box-line","notification-off-line","arrow-right-s-fill","aliens-line","notification-off-fill","arrow-right-s-line","body-scan-fill","pause-circle-fill","arrow-up-circle-fill","contacts-fill","arrow-up-circle-line","body-scan-line","pause-circle-line","arrow-up-down-fill","contacts-line","arrow-up-fill","notification-fill","arrow-up-line","arrow-up-down-line","arrow-right-up-line","notification-line","emotion-2-line","mv-line","emotion-happy-fill","pause-fill","checkbox-blank-circle-fill","emotion-line","music-2-fill","checkbox-blank-circle-line","emotion-fill","pause-mini-fill","check-double-fill","emotion-laugh-line","mic-2-line","check-fill","emotion-laugh-fill","pause-mini-line","checkbox-blank-line","emotion-normal-line","phone-camera-fill","checkbox-blank-fill","emotion-happy-line","order-play-fill","checkbox-circle-line","emotion-normal-fill","checkbox-circle-fill","phone-camera-line","checkbox-fill","genderless-line","checkbox-indeterminate-fill","ghost-2-fill","checkbox-line","ghost-smile-fill","genderless-fill","picture-in-picture-fill","checkbox-indeterminate-line","ghost-fill","play-circle-fill","emotion-unhappy-line","checkbox-multiple-line","play-list-2-fill","close-circle-fill","play-line","checkbox-multiple-fill","group-2-fill","play-list-2-line","close-circle-line","men-line","dashboard-fill","emotion-2-fill","play-list-line","dashboard-line","men-fill","play-list-fill","delete-back-2-fill","group-2-line","polaroid-line","delete-back-2-line","radio-2-fill","delete-back-fill","open-arm-fill","polaroid-2-line","delete-bin-2-fill","polaroid-fill","delete-bin-2-line","delete-back-line","parent-line","polaroid-2-fill","delete-bin-3-fill","robot-fill","radio-fill","delete-bin-4-fill","radio-2-line","delete-bin-4-line","emotion-unhappy-fill","radio-line","delete-bin-3-line","skull-fill","record-circle-line","delete-bin-5-fill","skull-2-line","record-circle-fill","delete-bin-5-line","skull-line","repeat-2-line","parent-fill","repeat-fill","robot-line","delete-bin-7-fill","skull-2-fill","repeat-one-fill","delete-bin-fill","spy-line","delete-bin-line","star-smile-fill","repeat-one-line","download-2-fill","divide-line","spy-fill","rewind-mini-fill","delete-bin-7-line","rhythm-fill","download-cloud-2-fill","star-smile-line","rewind-mini-line","download-cloud-line","travesti-line","download-cloud-fill","user-2-fill","shuffle-line","download-fill","travesti-fill","shuffle-fill","user-2-line","external-link-line","skip-back-mini-line","user-4-fill","divide-fill","user-4-line","external-link-fill","user-5-line","sound-module-fill","eye-2-line","user-5-fill","download-cloud-2-line","user-6-fill","speaker-2-fill","user-fill","speaker-2-line","arrow-up-s-fill","user-follow-fill","speaker-3-fill","filter-2-fill","user-follow-line","sound-module-line","filter-2-line","user-heart-line","speaker-fill","user-heart-fill","play-circle-line","keyboard-box-line","no-repeat-2","play-fill","repeat-2-fill","refresh-line","scissors-cut-line","building-line","character-recognition-fill","close-line","check-line","character-recognition-line","checkbox-multiple-blank-line","delete-bin-6-fill","community-line","delete-bin-6-line","download-cloud-fill","checkbox-multiple-blank-fill","candle-line","community-fill","download-fill","download-2-line","download-cloud-line","dv-fill","download-line","edit-2-fill","dv-line","error-warning-fill","edit-2-line","equalizer-fill","equalizer-line","earth-line","error-warning-line","eye-fill","earth-fill","eye-close-line","eye-close-fill","file-add-fill","file-search-line","file-search-fill","film-fill","eye-off-fill","file-download-line","fullscreen-exit-line","flag-fill","group-line","film-line","file-download-fill","eye-line","file-list-3-fill","group-fill","headphone-fill","file-add-line","filter-fill","file-list-3-line","flag-line","fullscreen-line","headphone-line","function-fill","function-line","information-fill","logout-box-r-line","lock-2-line","eye-off-line","key-fill","filter-line","key-line","mail-add-fill","lock-2-fill","mail-add-line","information-line","mail-line","map-pin-fill","loop-right-line","map-pin-line","image-line","mail-fill","map-2-line","map-2-fill","map-pin-range-fill","map-pin-range-line","menu-fold-line","men-line","menu-unfold-fill","menu-unfold-line","movie-2-line","menu-add-line","mic-fill","mic-off-fill","menu-fold-fill","movie-2-fill","mic-line","mic-off-line","pause-line","notification-3-line","notification-3-fill","picture-in-picture-2-fill","question-fill","question-line","play-line","reply-fill","picture-in-picture-exit-fill","picture-in-picture-line","rewind-line","restart-line","reply-line","profile-line","save-3-line","picture-in-picture-exit-line","picture-in-picture-2-line","image-fill","route-line","save-3-fill","profile-fill","plane-line","rewind-fill","restart-fill","route-fill","share-forward-2-fill","seedling-line","seedling-fill","settings-4-fill","logout-box-r-fill","share-forward-line","search-line","search-fill","share-forward-fill","shut-down-fill","shield-star-line","shield-star-fill","shut-down-line","skip-back-line","skip-forward-fill","speed-line","stop-line","slideshow-3-fill","settings-4-line","speed-fill","skip-back-fill","subtract-line","skip-forward-line","slideshow-3-line","team-line","sword-fill","time-line","sword-line","time-fill","team-fill","upload-fill","upload-line","user-3-fill","user-3-line","upload-cloud-2-line","user-add-line","stop-fill","user-add-fill","upload-cloud-2-fill","upload-2-line","volume-up-line","voice-recognition-line","user-voice-line","volume-mute-fill","women-line","user-voice-fill","voice-recognition-fill","volume-mute-line","voiceprint-line","zoom-in-fill","volume-up-fill","zoom-in-line","zoom-out-line","zoom-out-fill","alarm-line","archive-drawer-fill","admin-fill","bank-line","aspect-ratio-fill","add-line","article-line","article-fill","building-fill","alarm-fill","archive-drawer-line","admin-line","aspect-ratio-line","bookmark-3-line","bank-fill","calendar-todo-fill","bookmark-3-fill","calendar-todo-line","candle-fill"] \ No newline at end of file From b5e7bccaf273aa87fce2395dc51ba9cf94fbed4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=95=B8?= Date: Mon, 24 Nov 2025 16:07:47 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ai-data-set/{ => demos}/table-col.json | 0 docs/ai-data-set/{ => demos}/table-empty.json | 0 docs/ai-data-set/{ => demos}/table-pager.json | 0 docs/ai-data-set/{ => demos}/table-slot.json | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename docs/ai-data-set/{ => demos}/table-col.json (100%) rename docs/ai-data-set/{ => demos}/table-empty.json (100%) rename docs/ai-data-set/{ => demos}/table-pager.json (100%) rename docs/ai-data-set/{ => demos}/table-slot.json (100%) diff --git a/docs/ai-data-set/table-col.json b/docs/ai-data-set/demos/table-col.json similarity index 100% rename from docs/ai-data-set/table-col.json rename to docs/ai-data-set/demos/table-col.json diff --git a/docs/ai-data-set/table-empty.json b/docs/ai-data-set/demos/table-empty.json similarity index 100% rename from docs/ai-data-set/table-empty.json rename to docs/ai-data-set/demos/table-empty.json diff --git a/docs/ai-data-set/table-pager.json b/docs/ai-data-set/demos/table-pager.json similarity index 100% rename from docs/ai-data-set/table-pager.json rename to docs/ai-data-set/demos/table-pager.json diff --git a/docs/ai-data-set/table-slot.json b/docs/ai-data-set/demos/table-slot.json similarity index 100% rename from docs/ai-data-set/table-slot.json rename to docs/ai-data-set/demos/table-slot.json From 66aa20248a1ff53a39f65b11d89cf984d3e12c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=95=B8?= Date: Wed, 17 Dec 2025 14:39:12 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84runtime=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../build/vite-config/src/runtime-external.js | 20 + packages/runtime-renderer/config.ts | 15 + packages/runtime-renderer/index.ts | 40 +- packages/runtime-renderer/package.json | 21 +- packages/runtime-renderer/readme.md | 46 ++ packages/runtime-renderer/src/App.vue | 2 +- .../src/app-function/dataSource.ts | 136 ----- .../src/app-function/http/axios.js | 143 ----- .../src/app-function/http/index.js | 28 - .../src/app-function/import-map.json | 6 - .../src/app-function/importMap.ts | 92 --- .../src/app-function/index.ts | 4 - .../src/app-function/nativeComponents.ts | 120 ---- .../src/components/BlockLoadError.vue | 22 + .../src/components/BlockLoading.vue | 14 + .../src/components/NotFound.vue | 4 +- .../src/components/PageRenderer.ts | 20 +- .../src/composables/service.ts | 74 +++ .../src/composables/useAppSchema.ts | 235 ++------ .../src/renderer/RenderMain.ts | 117 +--- .../src/renderer/app-function/constant.ts | 4 + .../renderer/app-function/dataSource/http.js | 69 +++ .../renderer/app-function/dataSource/index.ts | 108 ++++ .../src/renderer/app-function/importMap.ts | 161 ++++++ .../src/renderer/app-function/index.ts | 7 + .../src/renderer/app-function/loadCompLib.ts | 63 ++ .../app-function/router.ts} | 19 +- .../app-function/store.ts} | 18 +- .../src/{ => renderer}/app-function/utils.ts | 4 +- .../src/renderer/builtin/CanvasBox.vue | 16 + .../src/renderer/builtin/CanvasRouterView.vue | 2 +- .../src/renderer/builtin/index.ts | 29 +- .../src/renderer/context/index.ts | 7 + .../src/renderer/context/useContext.ts | 122 ++++ .../context/useDataSource.ts} | 9 +- .../src/renderer/context/useLowcode.ts | 91 +++ .../src/renderer/context/useMethods.ts | 22 + .../src/renderer/context/useRefs.ts | 10 + .../src/renderer/context/useState.ts | 37 ++ .../src/renderer/context/useStore.ts | 8 + .../src/renderer/context/useUtils.ts | 9 + .../{parser => data-function}/index.ts | 1 + .../{parser => data-function}/parser.ts | 286 ++++----- .../data-function/utils.ts} | 11 +- .../material-function/blockComplier.ts | 33 ++ .../src/renderer/material-function/index.ts | 2 + .../material-function/material-getter.ts | 122 ++++ .../src/renderer/page-function/accessor.ts | 2 +- .../renderer/page-function/blockContext.ts | 92 --- .../src/renderer/page-function/css.ts | 165 ++---- .../src/renderer/page-function/index.ts | 2 +- .../src/renderer/page-function/lifecyle.ts | 94 +++ .../src/renderer/page-function/state.ts | 46 -- .../runtime-renderer/src/renderer/render.ts | 542 +++++------------- .../src/renderer/useContext.ts | 42 -- packages/runtime-renderer/src/types/index.ts | 31 + packages/runtime-renderer/src/types/schema.ts | 12 + packages/runtime-renderer/types.d.ts | 6 + packages/runtime-renderer/vite.config.ts | 19 +- 59 files changed, 1722 insertions(+), 1760 deletions(-) create mode 100644 packages/build/vite-config/src/runtime-external.js create mode 100644 packages/runtime-renderer/config.ts create mode 100644 packages/runtime-renderer/readme.md delete mode 100644 packages/runtime-renderer/src/app-function/dataSource.ts delete mode 100644 packages/runtime-renderer/src/app-function/http/axios.js delete mode 100644 packages/runtime-renderer/src/app-function/http/index.js delete mode 100644 packages/runtime-renderer/src/app-function/import-map.json delete mode 100644 packages/runtime-renderer/src/app-function/importMap.ts delete mode 100644 packages/runtime-renderer/src/app-function/index.ts delete mode 100644 packages/runtime-renderer/src/app-function/nativeComponents.ts create mode 100644 packages/runtime-renderer/src/components/BlockLoadError.vue create mode 100644 packages/runtime-renderer/src/components/BlockLoading.vue create mode 100644 packages/runtime-renderer/src/composables/service.ts create mode 100644 packages/runtime-renderer/src/renderer/app-function/constant.ts create mode 100644 packages/runtime-renderer/src/renderer/app-function/dataSource/http.js create mode 100644 packages/runtime-renderer/src/renderer/app-function/dataSource/index.ts create mode 100644 packages/runtime-renderer/src/renderer/app-function/importMap.ts create mode 100644 packages/runtime-renderer/src/renderer/app-function/index.ts create mode 100644 packages/runtime-renderer/src/renderer/app-function/loadCompLib.ts rename packages/runtime-renderer/src/{router/index.ts => renderer/app-function/router.ts} (86%) rename packages/runtime-renderer/src/{stores/index.ts => renderer/app-function/store.ts} (80%) rename packages/runtime-renderer/src/{ => renderer}/app-function/utils.ts (95%) create mode 100644 packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue create mode 100644 packages/runtime-renderer/src/renderer/context/index.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useContext.ts rename packages/runtime-renderer/src/{app-function/http/config.js => renderer/context/useDataSource.ts} (75%) create mode 100644 packages/runtime-renderer/src/renderer/context/useLowcode.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useMethods.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useRefs.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useState.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useStore.ts create mode 100644 packages/runtime-renderer/src/renderer/context/useUtils.ts rename packages/runtime-renderer/src/renderer/{parser => data-function}/index.ts (51%) rename packages/runtime-renderer/src/renderer/{parser => data-function}/parser.ts (53%) rename packages/runtime-renderer/src/{utils/data-utils.ts => renderer/data-function/utils.ts} (79%) create mode 100644 packages/runtime-renderer/src/renderer/material-function/blockComplier.ts create mode 100644 packages/runtime-renderer/src/renderer/material-function/index.ts create mode 100644 packages/runtime-renderer/src/renderer/material-function/material-getter.ts delete mode 100644 packages/runtime-renderer/src/renderer/page-function/blockContext.ts create mode 100644 packages/runtime-renderer/src/renderer/page-function/lifecyle.ts delete mode 100644 packages/runtime-renderer/src/renderer/page-function/state.ts delete mode 100644 packages/runtime-renderer/src/renderer/useContext.ts create mode 100644 packages/runtime-renderer/src/types/index.ts diff --git a/packages/build/vite-config/src/runtime-external.js b/packages/build/vite-config/src/runtime-external.js new file mode 100644 index 000000000..8c32d007d --- /dev/null +++ b/packages/build/vite-config/src/runtime-external.js @@ -0,0 +1,20 @@ +export const prefix = '/node_modules/@opentiny/tiny-engine' +export const dependencies = { + base: { + imports: { + vue: `${prefix}/node_modules/vue/dist/vue.runtime.esm-browser.js`, + 'vue-i18n': `${prefix}/node_modules/vue-i18n/dist/vue-i18n.esm-browser.js` + }, + externals: [/^vue$/, /^vue-i18n$/] + }, + ui: { + imports: { + '@opentiny/vue': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-pc.mjs`, + '@opentiny/vue-icon': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-icon.mjs`, + '@opentiny/vue-common': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-common.mjs`, + '@opentiny/vue-locale': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-locale.mjs` + }, + externals: [/^@opentiny\/vue$/, /^@opentiny\/vue-icon$/, /^@opentiny\/vue-common$/, /^@opentiny\/vue-locale$/], + importStyles: [`${prefix}/node_modules/@opentiny/vue-theme/index.css`] + } +} diff --git a/packages/runtime-renderer/config.ts b/packages/runtime-renderer/config.ts new file mode 100644 index 000000000..f3f9fdecc --- /dev/null +++ b/packages/runtime-renderer/config.ts @@ -0,0 +1,15 @@ +export function useEnv(): Record { + const env = import.meta.env + return { ...env } +} + +export const defaultConfig = { + material: ['/mock/bundle.json'], + importMap: { + imports: {} + }, + // 是否开启 TailWindCSS 特性 + enableTailwindCSS: true +} + +export default defaultConfig diff --git a/packages/runtime-renderer/index.ts b/packages/runtime-renderer/index.ts index 6c15fdb03..ab11da279 100644 --- a/packages/runtime-renderer/index.ts +++ b/packages/runtime-renderer/index.ts @@ -11,45 +11,29 @@ */ import { createApp } from 'vue' +import defaultConfig from './config' +import { createAppRouter, createAppStores } from './src/renderer/app-function' import { useAppSchema } from './src/composables/useAppSchema' -import { createAppRouter } from './src/router' -import { createPinia } from 'pinia' -import { createStores, generateStoresConfig } from './src/stores' +import i18n from '@opentiny/tiny-engine-common/js/i18n' import App from './src/App.vue' -import i18n from '@opentiny/tiny-engine-i18n-host' // 初始化运行时渲染器 -export const initRuntimeRenderer = async () => { +export const initRuntimeRenderer = async (config: any) => { + Object.assign(defaultConfig, config || {}) const searchParams = new URLSearchParams(location.search) const appId = searchParams.get('id') - if (!appId) { throw new Error('Missing required "id" query parameter') } - - const { fetchAppSchema, fetchBlocks } = useAppSchema() - await fetchAppSchema(appId) - await fetchBlocks() - const router = await createAppRouter() - - const pinia = createPinia() - const storesConfig = generateStoresConfig() - const stores = createStores(storesConfig, pinia) - + const { initAppData } = useAppSchema() + await initAppData(appId) + const router = createAppRouter() + const pinia = createAppStores() const app = createApp(App) - app.provide('stores', stores) - + app.use(pinia).use(i18n).use(router).mount('#app') // 全局错误处理(防止 scheduler 被打断) - app.config.errorHandler = (err, instance, info) => { + app.config.errorHandler = (err, _instance, info) => { // eslint-disable-next-line no-console - console.error('[GlobalErrorHandler]', err, info) - if ((err as any)?.stack) { - // eslint-disable-next-line no-console - console.error('[GlobalErrorHandler stack]', (err as any).stack) - } + console.error(err, _instance, info) } - - app.use(pinia).use(router).use(i18n).mount('#app') - - return app } diff --git a/packages/runtime-renderer/package.json b/packages/runtime-renderer/package.json index 422a7d1da..62fc69382 100644 --- a/packages/runtime-renderer/package.json +++ b/packages/runtime-renderer/package.json @@ -15,15 +15,14 @@ }, "dependencies": { "@opentiny/tiny-engine-builtin-component": "workspace:*", - "@opentiny/tiny-engine-i18n-host": "workspace:*", - "@vue/babel-plugin-jsx": "^1.2.5", - "@babel/core": "^7.18.13", - "@vue/shared": "^3.3.4", + "@opentiny/tiny-engine-block-compiler": "workspace:*", + "@opentiny/tiny-engine-dsl-vue": "workspace:*", + "@opentiny/tiny-engine-common": "workspace:*", + "@opentiny/tiny-engine-utils": "workspace:*", "axios": "^1.10.0", - "axios-mock-adapter": "^1.19.0", "pinia": "^2.1.0", - "vue-router": "^4.2.0", "postcss": "^8.4.31", + "axios-mock-adapter": "^1.19.0", "postcss-selector-parser": "^7.0.0" }, "devDependencies": { @@ -32,11 +31,15 @@ "vite": "^5.4.2" }, "peerDependencies": { - "@opentiny/vue": "^3.20.0", - "@opentiny/vue-icon": "^3.20.0", + "@babel/core": "^7.26.0", + "@opentiny/vue": "~3.20.0", + "@opentiny/vue-icon": "~3.20.0", "@opentiny/vue-locale": "~3.20.0", "@opentiny/vue-theme": "~3.20.0", + "@vue/babel-plugin-jsx": "^1.2.5", + "@vue/shared": "^3.3.4", "vue": "^3.4.15", - "vue-i18n": "^9.9.0" + "vue-i18n": "^9.9.0", + "vue-router": "^4.2.0" } } diff --git a/packages/runtime-renderer/readme.md b/packages/runtime-renderer/readme.md new file mode 100644 index 000000000..8874ed606 --- /dev/null +++ b/packages/runtime-renderer/readme.md @@ -0,0 +1,46 @@ +# 运行时渲染器-在线应用预览 + +## 功能目录结构 + +packages/runtime-renderer +├── config.ts 配置相关 +├── index.ts 运行时渲染器入口文件 +├── package.json 依赖记录 +├── src/ 源代码目录 +│ ├── App.vue APP.vue入口 +│ ├── components/ 通用组件 +│ ├── composables/ 应用数据记录相关 +│ ├── renderer/ +│ │ ├── app-function/ 应用通用函数 +│ │ ├── builtin/ 染器内部组件(待迁移至builtinComponent包) +│ │ ├── context/ 页面运行时上下文 +│ │ ├── data-function/ 数据转换通用函数 +│ │ ├── material-function/ 物料通用函数 +│ │ ├── page-function/ 页面通用函数 +│ │ ├── RenderMain.ts 渲染器入口 +│ │ └── render.ts 渲染器组件 +│ └── types/ TS类型定义 +├── types.d.ts TS类型扩展 +└── vite.config.ts 编译配置 + +## 渲染器架构图 + +![渲染器架构图](./renderer-arch.png) + +## 启动开发 + +```bash +pnpm i + +pnpm dev +``` + +## 生产打包 + +```bash +pnpm i + +pnpm build:plugin + +pnpm build:prod +``` diff --git a/packages/runtime-renderer/src/App.vue b/packages/runtime-renderer/src/App.vue index ee8caf2c4..98240aef8 100644 --- a/packages/runtime-renderer/src/App.vue +++ b/packages/runtime-renderer/src/App.vue @@ -1,3 +1,3 @@ diff --git a/packages/runtime-renderer/src/app-function/dataSource.ts b/packages/runtime-renderer/src/app-function/dataSource.ts deleted file mode 100644 index 2de82554b..000000000 --- a/packages/runtime-renderer/src/app-function/dataSource.ts +++ /dev/null @@ -1,136 +0,0 @@ -import useHttp from './http' -import { parseJSFunction } from '../utils/data-utils' -// 将原本的配置格式标准化以方便复用出码逻辑 -const normalizeItem = (item: any) => { - return { - id: item.id, - name: item.name, - columns: item.data.columns, - data: item.data.data, - type: item.data.type, - options: item.data.options, - dataHandler: item.data.dataHandler, - willFetch: item.data.willFetch, - shouldFetch: item.data.shouldFetch, - errorHandler: item.data.errorHandler - } -} - -const dataSourceMap: Record = {} - -let globalDataHandle: (res: any) => any = (res) => res - -// 统一的 load 构造 -const load = (http, options, dataSource, shouldFetch, parsedDataHandler) => (params?, customUrl?) => { - // 无 options 视为本地/静态数据 - if (!options) { - try { - let raw = globalDataHandle(dataSource.config.data) - if (parsedDataHandler) { - raw = parsedDataHandler(raw) - } - const items = Array.isArray(raw) ? raw : raw ? [raw] : [] - const wrapped = { code: '', msg: 'success', data: { items, total: items.length } } - dataSource.status = 'loaded' - dataSource.data = wrapped - return Promise.resolve(wrapped) - } catch (e) { - dataSource.status = 'error' - dataSource.error = e - return Promise.reject(e) - } - } - - if (!shouldFetch()) { - return Promise.resolve(undefined) - } - - dataSource.status = 'loading' - const { method = 'GET', uri: url, params: defaultParams, timeout, headers } = options - const config: any = { method, url, headers, timeout } - - const data = params || defaultParams - config.url = customUrl || config.url - - if (method.toLowerCase() === 'get') { - config.params = data - } else { - config.data = data - } - - return http.request(config) -} - -export const initDataSource = (config: any) => { - Object.keys(dataSourceMap).forEach((key) => delete dataSourceMap[key]) - - if (!config) { - return dataSourceMap - } - - const normalized = (config.list || []).map(normalizeItem) - - globalDataHandle = config.dataHandler ? parseJSFunction(config.dataHandler) : (res) => res - - normalized.forEach((item) => { - const http = useHttp(globalDataHandle) - const dataSource = { - config: item, - status: 'init', - data: { data: item.data }, - load: null // 将在下面设置 - } - - const shouldFetch = item.shouldFetch?.value ? parseJSFunction(item.shouldFetch) : () => true - const willFetch = item.willFetch?.value ? parseJSFunction(item.willFetch) : (options) => options - const parsedDataHandler = item.dataHandler?.value ? parseJSFunction(item.dataHandler) : null - const parsedErrorHandler = item.errorHandler?.value ? parseJSFunction(item.errorHandler) : null - - const dataHandler = (res) => { - // 先应用用户定义的dataHandler - const handled = parsedDataHandler ? parsedDataHandler(res) : res - - // 统一数据结构,确保与远程数据源与静态数据源结构一致 - let unifiedData - if (handled && typeof handled === 'object' && !handled.data?.items) { - // 如果不是静态数据源的格式,则转换为统一格式 - const items = Array.isArray(handled.data) ? handled.data : handled.data ? [handled.data] : [] - unifiedData = { - code: handled.code || '', - msg: handled.message || handled.msg || 'success', - data: { - items, - total: items.length - } - } - } else { - unifiedData = handled - } - - dataSource.status = 'loaded' - dataSource.data = unifiedData - return unifiedData - } - const errorHandler = (error) => { - if (parsedErrorHandler) { - parsedErrorHandler(error) - } - dataSource.status = 'error' - dataSource.error = error - return Promise.reject(error) - } - - http.interceptors.request.use(willFetch, errorHandler) - http.interceptors.response.use(dataHandler, errorHandler) - - // 设置 load 方法,传递 parsedDataHandler 参数 - dataSource.load = load(http, item.options, dataSource, shouldFetch, parsedDataHandler) - - // 存储到映射中 - dataSourceMap[item.name] = dataSource - }) -} - -export const getDataSource = () => dataSourceMap - -export default dataSourceMap diff --git a/packages/runtime-renderer/src/app-function/http/axios.js b/packages/runtime-renderer/src/app-function/http/axios.js deleted file mode 100644 index 3654c619b..000000000 --- a/packages/runtime-renderer/src/app-function/http/axios.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -import axios from 'axios' -import MockAdapter from 'axios-mock-adapter' - -export default (config) => { - const instance = axios.create(config) - const defaults = {} - let mock - - if (typeof MockAdapter.prototype.proxy === 'undefined') { - MockAdapter.prototype.proxy = function ({ url, config = {}, proxy, response, handleData } = {}) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let stream = this - const request = (proxy, any) => { - return (setting) => { - return new Promise((resolve) => { - config.responseType = 'json' - axios - .get(any ? proxy + setting.url + '.json' : proxy, config) - .then(({ data }) => { - if (typeof handleData === 'function') { - data = handleData.call(null, data, setting) - } - resolve([200, data]) - }) - .catch((error) => { - resolve([error.response.status, error.response.data]) - }) - }) - } - } - - if (url === '*' && proxy && typeof proxy === 'string') { - stream = proxy === '*' ? this.onAny().passThrough() : this.onAny().reply(request(proxy, true)) - } else { - if (proxy && typeof proxy === 'string') { - stream = this.onAny(url).reply(request(proxy)) - } else if (typeof response === 'function') { - stream = this.onAny(url).reply(response) - } - } - - return stream - } - } - - return { - request(config) { - return instance(config) - }, - get(url, config) { - return instance.get(url, config) - }, - delete(url, config) { - return instance.delete(url, config) - }, - head(url, config) { - return instance.head(url, config) - }, - post(url, data, config) { - return instance.post(url, data, config) - }, - put(url, data, config) { - return instance.put(url, data, config) - }, - patch(url, data, config) { - return instance.patch(url, data, config) - }, - all(iterable) { - return axios.all(iterable) - }, - spread(callback) { - return axios.spread(callback) - }, - defaults(key, value) { - if (key && typeof key === 'string') { - if (typeof value === 'undefined') { - return instance.defaults[key] - } - instance.defaults[key] = value - defaults[key] = value - } else { - return instance.defaults - } - }, - defaultSettings() { - return defaults - }, - interceptors: { - request: { - use(fnHandle, fnError) { - return instance.interceptors.request.use(fnHandle, fnError) - }, - eject(id) { - return instance.interceptors.request.eject(id) - } - }, - response: { - use(fnHandle, fnError) { - return instance.interceptors.response.use(fnHandle, fnError) - }, - eject(id) { - return instance.interceptors.response.eject(id) - } - } - }, - mock(config) { - if (!mock) { - mock = new MockAdapter(instance) - } - - if (Array.isArray(config)) { - config.forEach((item) => { - mock.proxy(item) - }) - } - - return mock - }, - disableMock() { - if (mock) { - mock.restore() - } - mock = undefined - }, - isMock() { - return typeof mock !== 'undefined' - }, - CancelToken: axios.CancelToken, - isCancel: axios.isCancel - } -} diff --git a/packages/runtime-renderer/src/app-function/http/index.js b/packages/runtime-renderer/src/app-function/http/index.js deleted file mode 100644 index 39f6776fe..000000000 --- a/packages/runtime-renderer/src/app-function/http/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -import axios from './axios' -import config from './config' - -export default (dataHandler) => { - const http = axios(config) - - http.interceptors.response.use(dataHandler, (error) => { - const response = error.response - if (response && response.status === 403 && response.headers && response.headers['x-login-url']) { - // TODO 处理无权限时,重新登录再发送请求 - } - return Promise.reject(error) - }) - - return http -} diff --git a/packages/runtime-renderer/src/app-function/import-map.json b/packages/runtime-renderer/src/app-function/import-map.json deleted file mode 100644 index 364ed31e2..000000000 --- a/packages/runtime-renderer/src/app-function/import-map.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "imports": { - "echarts": "${VITE_CDN_DOMAIN}/echarts${versionDelimiter}5.4.1${fileDelimiter}/dist/echarts.esm.js" - }, - "importStyles": {} -} diff --git a/packages/runtime-renderer/src/app-function/importMap.ts b/packages/runtime-renderer/src/app-function/importMap.ts deleted file mode 100644 index d6b30f4aa..000000000 --- a/packages/runtime-renderer/src/app-function/importMap.ts +++ /dev/null @@ -1,92 +0,0 @@ -import importMapConfig from './import-map.json' - -const IMPORT_MAP_ELEMENT_ID = 'tiny-engine-runtime-import-map' - -type ImportMapConfig = { - imports?: Record - importStyles?: Record -} - -const DEFAULT_ENV = { - VITE_CDN_TYPE: 'npmmirror', - VITE_CDN_DOMAIN: 'https://registry.npmmirror.com', - VITE_LOCAL_IMPORT_PATH: 'local-cdn-static', - BASE_URL: '/', - VITE_LOCAL_IMPORT_MAPS: 'false' -} - -const getEnvValue = (key: keyof typeof DEFAULT_ENV) => { - return DEFAULT_ENV[key] -} - -const replacePlaceholder = (value: string) => { - const VITE_CDN_TYPE = getEnvValue('VITE_CDN_TYPE') - const VITE_CDN_DOMAIN = getEnvValue('VITE_CDN_DOMAIN') - const VITE_LOCAL_IMPORT_PATH = getEnvValue('VITE_LOCAL_IMPORT_PATH') - const BASE_URL = getEnvValue('BASE_URL') - const VITE_LOCAL_IMPORT_MAPS = getEnvValue('VITE_LOCAL_IMPORT_MAPS') - - const isLocalBundle = String(VITE_LOCAL_IMPORT_MAPS) === 'true' - const versionDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/' : '@' - const fileDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/files' : '' - const cdnDomain = isLocalBundle ? `${BASE_URL}${VITE_LOCAL_IMPORT_PATH}` : VITE_CDN_DOMAIN - - return value - .replace('${VITE_CDN_DOMAIN}', cdnDomain) - .replace('${versionDelimiter}', versionDelimiter) - .replace('${fileDelimiter}', fileDelimiter) -} - -const parseImportMapConfig = (config: ImportMapConfig) => { - const imports: Record = {} - const importStyles: string[] = [] - - Object.entries(config.imports || {}).forEach(([name, url]) => { - imports[name] = replacePlaceholder(url) - }) - - Object.values(config.importStyles || {}).forEach((url) => { - const parsed = replacePlaceholder(url) - importStyles.push(parsed) - }) - - return { imports, importStyles } -} - -const mergeImportMap = (imports: Record) => { - if (typeof document === 'undefined') { - return - } - - const existing = document.getElementById(IMPORT_MAP_ELEMENT_ID) as HTMLScriptElement | null - const mergedImports: Record = {} - - if (existing?.textContent) { - try { - const parsed = JSON.parse(existing.textContent) - Object.assign(mergedImports, parsed?.imports || {}) - } catch (err) { - // eslint-disable-next-line no-console - console.warn('[runtime-import-map] 解析已有 import map 失败,将覆盖为新的配置。', err) - } - } - - Object.assign(mergedImports, imports) - - const importMapScript = existing || document.createElement('script') - importMapScript.type = 'importmap' - importMapScript.id = IMPORT_MAP_ELEMENT_ID - importMapScript.textContent = JSON.stringify({ imports: mergedImports }, null, 2) - - if (!existing) { - document.head.appendChild(importMapScript) - } -} - -export const initImportMap = () => { - const { imports: baseImports } = parseImportMapConfig(importMapConfig) - - mergeImportMap({ ...baseImports }) -} - -export default initImportMap diff --git a/packages/runtime-renderer/src/app-function/index.ts b/packages/runtime-renderer/src/app-function/index.ts deleted file mode 100644 index bbd3065b3..000000000 --- a/packages/runtime-renderer/src/app-function/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './dataSource' -export * from './nativeComponents' -export * from './utils' -export * from './importMap' diff --git a/packages/runtime-renderer/src/app-function/nativeComponents.ts b/packages/runtime-renderer/src/app-function/nativeComponents.ts deleted file mode 100644 index 7449cfc1d..000000000 --- a/packages/runtime-renderer/src/app-function/nativeComponents.ts +++ /dev/null @@ -1,120 +0,0 @@ -// 定义组件配置接口 -interface ComponentConfig { - destructuring?: boolean - exportName?: string -} - -// 定义组件依赖接口 -interface ComponentDependency { - package?: string | null - components: Record - npmrc?: any -} - -// 定义动态导入参数接口 -interface DynamicImportParams { - package: string - script?: string -} - -export const addStyle = (href: string, doc = document): Promise => { - return new Promise((resolve, reject) => { - const link = doc.createElement('link') - - link.setAttribute('href', href) - link.setAttribute('rel', 'stylesheet') - - link.onload = resolve - link.onerror = reject - - doc.querySelector('head')!.appendChild(link) - }) -} - -/** - * 动态导入获取组件库模块 - * @param {DynamicImportParams} param 模块参数,包含pkg模块名称和script模块的cdn地址 - * @returns {Promise} 返回组件库模块 - */ -const dynamicImportComponentLib = async ({ package: pkg, script }: DynamicImportParams): Promise => { - if (window.TinyComponentLibs[pkg]) { - return window.TinyComponentLibs[pkg] - } - - if (!script) { - return {} - } - - const href = window.location.href - const scriptUrl = script.startsWith('.') ? new URL(script, href).href : script - - try { - if (!window.TinyComponentLibs[pkg]) { - const modules = await import(/* @vite-ignore */ scriptUrl) - - window.TinyComponentLibs[pkg] = modules - } - } catch (error) { - // eslint-disable-next-line no-console - console.log(`加载组件库失败: ${pkg}`, error) - } - - return window.TinyComponentLibs[pkg] -} - -/** - * 获取组件库的package依赖 - * @param {DynamicImportParams[]} packageDependencys 组件库的package依赖数组 - * @returns {Promise} 返回组件库的package依赖对象 - */ -export const loadPackageDependencys = async (packageDependencys: DynamicImportParams[]): Promise => { - for (const packageDependency of packageDependencys) { - const { package: pkg, script } = packageDependency - if (pkg === '@opentiny/vue') continue - await dynamicImportComponentLib({ package: pkg, script }) - } -} - -export const getComponentLibs = async (pkg: string, npmrc?: string) => { - if (window.TinyComponentLibs[pkg]) { - return window.TinyComponentLibs[pkg] - } else { - // 如果组件包含npmrc字段,则尝试从npmrc中引入模块 - if (npmrc && npmrc !== 'null' && npmrc !== '') { - try { - const modules = await import(/* @vite-ignore */ npmrc) - window.TinyComponentLibs[pkg] = modules - return modules - } catch (error) { - // eslint-disable-next-line no-console - console.error(`从 npmrc 加载组件库失败: ${pkg}`, error) - } - } - throw new Error(`${pkg} 组件库未找到`) - } -} - -/** - * 获取组件对象并缓存,组件渲染时使用 - * @param {ComponentDependency} param 组件的依赖配置对象 - * @returns {Promise} 无返回值的Promise - */ -export const getComponents = async ({ package: pkg, components, npmrc }: ComponentDependency): Promise => { - if (!pkg || pkg === '@opentiny/vue') return - - const modules = await getComponentLibs(pkg, npmrc) - - Object.entries(components).forEach(([componentId, item]) => { - if (!window.TinyLowcodeComponent[componentId]) { - // 兼容老版本 - 当item是字符串时,直接作为模块导出名使用 - if (typeof item === 'string') { - window.TinyLowcodeComponent[componentId] = modules[item] - } else { - // 当item是配置对象时,根据destructuring属性决定如何获取组件 - const config = item as ComponentConfig - window.TinyLowcodeComponent[componentId] = - config?.destructuring && config?.exportName ? modules[config.exportName] : modules?.default - } - } - }) -} diff --git a/packages/runtime-renderer/src/components/BlockLoadError.vue b/packages/runtime-renderer/src/components/BlockLoadError.vue new file mode 100644 index 000000000..5a210e079 --- /dev/null +++ b/packages/runtime-renderer/src/components/BlockLoadError.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/packages/runtime-renderer/src/components/BlockLoading.vue b/packages/runtime-renderer/src/components/BlockLoading.vue new file mode 100644 index 000000000..b74bebdc2 --- /dev/null +++ b/packages/runtime-renderer/src/components/BlockLoading.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/runtime-renderer/src/components/NotFound.vue b/packages/runtime-renderer/src/components/NotFound.vue index fa8b95981..2ce993ed2 100644 --- a/packages/runtime-renderer/src/components/NotFound.vue +++ b/packages/runtime-renderer/src/components/NotFound.vue @@ -21,8 +21,8 @@
-

页面未找到

-

抱歉,您访问的页面不存在或已被删除。

+

应用没有设置主页

+

请手动补全应用路由去访问页面、或者在编辑页设置应用主页

请求的路径: {{ currentPath }}

diff --git a/packages/runtime-renderer/src/components/PageRenderer.ts b/packages/runtime-renderer/src/components/PageRenderer.ts index f8a1145ec..2fe6ae19f 100644 --- a/packages/runtime-renderer/src/components/PageRenderer.ts +++ b/packages/runtime-renderer/src/components/PageRenderer.ts @@ -1,25 +1,11 @@ import { defineComponent, h } from 'vue' import RenderMain from '../renderer/RenderMain' -export function withPageRenderer(WrappedComponent: any) { +export function withPageRenderer(props: any) { return defineComponent({ name: 'PageRendererHOC', - props: { - pageId: { - type: String, - required: true - } - }, - setup(props) { - return () => { - return h(WrappedComponent, { - pageId: props.pageId - }) - } + render() { + return h(RenderMain, { pageId: props.pageId, key: props.pageId }) } }) } - -// 默认导出 -const PageRendererHOC = withPageRenderer(RenderMain) -export default PageRendererHOC diff --git a/packages/runtime-renderer/src/composables/service.ts b/packages/runtime-renderer/src/composables/service.ts new file mode 100644 index 000000000..7481b930f --- /dev/null +++ b/packages/runtime-renderer/src/composables/service.ts @@ -0,0 +1,74 @@ +import type { BlockItem, IBlockItem } from '../types' + +export async function fetchAppSchema(id: string | number) { + let appSchema = {} + try { + const res: any = await fetch(`/app-center/v1/api/apps/schema/${id}`) + appSchema = (await res?.json()?.data) || {} + } catch (error) { + // eslint-disable-next-line no-console + console.error('获取应用Schema信息错误:', error) + } + return appSchema +} + +export async function fetchAppPackages(pkgUrl: string) { + let packages = [] + try { + const res = await fetch(pkgUrl) + const bundleJson: any = await res.json() + packages = bundleJson?.data?.materials?.packages || [] + } catch (error) { + // eslint-disable-next-line no-console + console.error('获取应用物料包错误:', error) + } + return packages +} + +export async function fetchAppPages(id: string | number) { + let pages = [] + try { + const res: any = await fetch(`/app-center/api/pages/list/${id}`) + pages = (await res.json()?.data) || [] + } catch (error) { + // eslint-disable-next-line no-console + console.error('获取应用页面错误:', error) + } + return pages +} +export async function fetchAllBlocks() { + const blocksMap: Record = {} + try { + const res: any = await fetch('/material-center/api/blocks') + const blocks: BlockItem[] = await res.json()?.data + blocks.forEach((block) => { + if (block.content) { + blocksMap[block.label] = { + schema: block.content, + meta: { + id: block.id, + label: block.label, + framework: block.framework, + version: block.version + } + } + } + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('获取所有区块错误:', error) + } + return blocksMap +} + +export async function fetchBlockByName(name: string) { + let block = {} + try { + const res: any = await fetch(`/material-center/api/block?label=${name}`) + block = await res.json()?.data?.[0] + } catch (error) { + // eslint-disable-next-line no-console + console.error(`获取区块[${name}]错误:`, error) + } + return block +} diff --git a/packages/runtime-renderer/src/composables/useAppSchema.ts b/packages/runtime-renderer/src/composables/useAppSchema.ts index a075ca189..4d8ae85c1 100644 --- a/packages/runtime-renderer/src/composables/useAppSchema.ts +++ b/packages/runtime-renderer/src/composables/useAppSchema.ts @@ -1,22 +1,9 @@ import { ref, computed, readonly } from 'vue' -import type { - IAppSchema, - Util, - BlockItem, - BlockContent, - I18nConfig, - ComponentMap, - PackageConfig -} from '../types/schema' -import i18n from '@opentiny/tiny-engine-i18n-host' -import { - addStyle, - getComponents, - loadPackageDependencys, - initDataSource, - initImportMap, - initUtils -} from '../app-function' +import type { IAppSchema, Util, I18nConfig, ComponentMap, PackageConfig } from '../types/index.ts' +import i18n from '@opentiny/tiny-engine-common/js/i18n' +import { addTagTask, getComponents, initDataSource, initImportMap, initUtils } from '../renderer/app-function/index.ts' +import config from '../../config.ts' +import { fetchAppSchema, fetchAppPackages, fetchAppPages, fetchAllBlocks, fetchBlockByName } from './service.ts' const appSchema = ref(null) const isLoading = ref(false) @@ -25,55 +12,29 @@ window.TinyLowcodeComponent = {} window.TinyComponentLibs = {} export function useAppSchema() { - let cachedBundlePackages: PackageConfig[] | null = null - - const loadBundlePackages = async () => { - if (cachedBundlePackages) { - return cachedBundlePackages - } - - try { - const response = await fetch('/mock/bundle.json') - - if (!response.ok) { - throw new Error(`加载基础物料包失败: HTTP ${response.status}: ${response.statusText}`) - } - - const bundleJson = await response.json() - - cachedBundlePackages = (bundleJson?.data?.materials?.packages as PackageConfig[]) || [] - } catch (error) { - // eslint-disable-next-line no-console - console.error('加载基础物料包失败:', error) - cachedBundlePackages = [] - } - - return cachedBundlePackages - } - - const initializeComponentsMap = async (componentsMap: ComponentMap[], packages: PackageConfig[]) => { + const initComponentsMap = async (componentsMap: ComponentMap[]) => { + const packages = appSchema.value?.packages || [] // 获取组件依赖 - const componentsDeps = componentsMap.map((component: ComponentMap) => ({ - package: component.package, - components: { - [component.componentName]: component.destructuring - ? { destructuring: true, exportName: component.exportName || component.componentName } - : component.componentName - }, - npmrc: component.npmrc + const componentsDeps: any = packages.map((pkg: PackageConfig) => ({ + ...pkg, + components: componentsMap.filter((comp) => comp.package === pkg.package) })) - const styles = packages.map((pkg) => pkg.css).filter((css) => css) as string[] - - await loadPackageDependencys(packages) - - try { - // 并行加载所有组件依赖和包资源,与 runner.ts 中的机制保持一致 - await Promise.all([...componentsDeps.map(getComponents), ...styles.map((src) => addStyle(src))]) - } catch (error) { - // eslint-disable-next-line no-console - console.error('组件或资源加载失败:', error) - } + await Promise.all([ + ...componentsDeps.map(getComponents), + ...styles.map((link) => + addTagTask({ + href: link, + tag: 'link', + type: config.enableTailwindCSS ? 'text/tailwindcss' : 'text/css' + }) + ) + ]) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('组件或资源加载失败:', err) + }) + .finally(() => {}) } // 初始化工具函数 @@ -86,15 +47,6 @@ export function useAppSchema() { } } - // 注入全局CSS - const injectGlobalCSS = (css: string) => { - if (!css) return - - const style = document.createElement('style') - style.textContent = css - document.head.appendChild(style) - } - const initializeI18n = (i18nConfig: I18nConfig) => { if (!i18nConfig) return Object.entries(i18nConfig).forEach(([loc, msgs]) => { @@ -102,118 +54,45 @@ export function useAppSchema() { }) } + // 注入全局CSS + const initGlobalCSS = async (css: string) => { + if (!css) return + await addTagTask({ + tag: 'style', + textContent: css, + id: 'app-global-css', + type: config.enableTailwindCSS ? 'text/tailwindcss' : 'text/css' + }) + } + // 初始化应用配置 - const initializeAppConfig = async (schema: IAppSchema) => { + const setAppConfig = async (schema: IAppSchema) => { if (!schema?.pages) return - - const packages = await loadBundlePackages() - // 初始化 importMap - initImportMap() - + await initImportMap() // 初始化除tinyVue之外的nativeComponents - await initializeComponentsMap(schema.componentsMap || [], packages) - - // 初始化国际化 - initializeI18n(schema?.i18n) - + await initComponentsMap(schema.componentsMap || []) // 初始化工具函数 await initializeUtils(schema?.utils) - + // 初始化国际化 + initializeI18n(schema?.i18n) // 初始化数据源 initDataSource(schema?.dataSource) - // 注入全局CSS - injectGlobalCSS(schema?.css) - } - - // 拉取完整应用schema - const fetchAppSchema = async (appId: string) => { - isLoading.value = true - error.value = null - - try { - const response = await fetch(`/app-center/v1/api/apps/schema/${appId}`) - - if (!response.ok) { - throw new Error(`加载应用Schema失败: HTTP ${response.status}: ${response.statusText}`) - } - - const data = await response.json() - - if (!data?.data) { - throw new Error('应用Schema数据无效') - } - - const response1 = await fetch(`/app-center/api/pages/list/${appId}`) - - if (!response1.ok) { - throw new Error(`加载页面Schema失败: HTTP ${response1.status}: ${response1.statusText}`) - } - - const res = await response1.json() - - appSchema.value = { ...data.data, pages: res?.data || [] } - // 解析并初始化应用级配置 - await initializeAppConfig(appSchema.value!) - } catch (err) { - error.value = err instanceof Error ? err.message : '加载应用Schema失败' - // eslint-disable-next-line no-console - console.error('加载应用Schema失败:', err) - } finally { - isLoading.value = false - } + initGlobalCSS(schema?.css) } - // 拉取区块schema - const fetchBlocks = async () => { - try { - const response = await fetch('/material-center/api/blocks') - - if (!response.ok) { - throw new Error(`加载区块Schema失败: HTTP ${response.status}: ${response.statusText}`) - } - - const blockJSON = await response.json() - - if (!Array.isArray(blockJSON?.data)) { - throw new Error('区块Schema数据无效') - } - - const blocks: BlockItem[] = blockJSON.data || [] - - // 转换为组件映射格式 - const blocksMap: Record< - string, - { - schema: BlockContent - meta: { - id: number - label: string - framework: string - version: string - } - } - > = {} - blocks.forEach((block) => { - if (block.content) { - blocksMap[block.label] = { - schema: block.content, - meta: { - id: block.id, - label: block.label, - framework: block.framework, - version: block.version - } - } - } - }) - - window.blocks = blocksMap - } catch (error) { - // eslint-disable-next-line no-console - console.error('加载区块Schema失败:', error) - } + const initAppData = async (id: string) => { + await Promise.all([ + fetchAppSchema(id), + fetchAppPages(id), + fetchAllBlocks(), + fetchAppPackages(config.material[0]) + ]).then((rss) => { + const [schema, pages, blocks, packages] = rss + appSchema.value = { ...schema, pages, blocks, packages } + }) + await setAppConfig(appSchema.value!) } // 获取页面列表 @@ -265,12 +144,8 @@ export function useAppSchema() { i18nConfig, // 方法 - fetchAppSchema, - fetchBlocks, + initAppData, getPageById, - - // 初始化方法 - initializeAppConfig, - injectGlobalCSS + fetchBlockByName } } diff --git a/packages/runtime-renderer/src/renderer/RenderMain.ts b/packages/runtime-renderer/src/renderer/RenderMain.ts index 205c3872f..5572fd59c 100644 --- a/packages/runtime-renderer/src/renderer/RenderMain.ts +++ b/packages/runtime-renderer/src/renderer/RenderMain.ts @@ -9,17 +9,13 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -import { h, computed, provide, nextTick, reactive, watch, defineComponent, inject } from 'vue' -import Loading from '../components/Loading.vue' -import { parseData } from './parser' -import { renderer } from './render.ts' -import { setPageCss, useState } from './page-function' -import useContext from './useContext.ts' -import { useRouter, useRoute } from 'vue-router' -import { useAppSchema } from '../composables/useAppSchema' -import type { PageContent as Schema } from '../types/schema' -import { getDataSource, getUtilsAll } from '../app-function' +import { defaultRenderer } from './render.ts' +import { useContextPage } from './context/index.ts' +import { useLowcode } from './context/useLowcode.ts' +import { useAppSchema } from '../composables/useAppSchema.ts' +import type { PageContent as Schema } from '../types/index.ts' +import { computed, provide, reactive, watch, defineComponent } from 'vue' +import { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n' interface Props { pageId: string @@ -29,85 +25,40 @@ export default defineComponent({ name: 'RenderMain', props: { pageId: { - type: String, + type: [String, Number], default: '0' } }, - setup(props: Props) { + setup(props: Props, ctx: any) { const { getPageById } = useAppSchema() - + // 通过 pageId 获取最新的页面对象 const currentSchema = computed(() => { - const page = getPageById(props.pageId) // 通过 pageId 获取最新的页面对象 + const page = getPageById(props.pageId) const pageContent = page?.page_content if (!pageContent) return null return JSON.parse(JSON.stringify(pageContent)) }) - - const route = useRoute() - const router = useRouter() - const { context, setContext, getContext } = useContext() - const reset = (obj: Record) => { - Object.keys(obj).forEach((key) => delete obj[key]) - } - const stores = inject('stores') - provide('pageContext', context) - const pageSchema = reactive({} as Schema) - const methods: Record = {} - const { state, setState } = useState({ getContext }) - - const setMethods = (data: Record = {}, clear?: boolean) => { - if (clear) reset(methods) - // 这里有些方法在画布还是有执行的必要的,比如说表格的renderer和formatText方法,包括一些自定义渲染函数 - Object.assign( - methods, - Object.fromEntries( - Object.keys(data).map((key) => { - return [key, parseData(data[key], {}, getContext())] - }) - ) - ) - setContext(methods) - } - - const setSchema = async (data: Schema) => { - if (!data) { - return - } - - const newSchema = JSON.parse(JSON.stringify(data)) - - const cssScopeId = `data-te-page-${String(props.pageId) || 'render-main'}` - const contextData = { - state, - route, - router, - stores, - dataSourceMap: getDataSource(), - utils: getUtilsAll(), - cssScopeId, - getCssScopeId: () => cssScopeId - } - // 此处提升很重要,因为setState、initProps也会触发画布重新渲染,所以需要提升上下文环境的设置时间 - setContext(contextData, true) - - // 设置方法调用上下文 - setMethods(newSchema.methods, true) - - // 这里setState(会触发画布渲染),是因为状态管理里面的变量会用到props、utils、bridge、stores、methods - setState(newSchema.state, true) - await nextTick() - setPageCss(data.css || '', cssScopeId) - Object.assign(pageSchema, newSchema) + // TODO 暂时置空解决区块编译后获取报错问题 + provide('page-ancestors', []) + // 提供翻译及区块Lowcode函数上下文 + const { TinyI18nHost } = useLowcode() + provide(I18nInjectionKey, TinyI18nHost) + // 提供页面级上下文 + const { state, methods, context, initContext } = useContextPage() + provide('pageContext', context) + const initPage = async (newSchema: Schema) => { + initContext({ schema: newSchema, props: props, ctx }, () => { + Object.assign(pageSchema, newSchema) + }) } - // 监听 schema 变化 watch( () => currentSchema.value, - async (schema) => { - if (!schema) return - if (Object.keys(schema).length === 0) return - await setSchema(schema) + (schema) => { + if (schema && Object.keys(schema).length !== 0) { + initPage(JSON.parse(JSON.stringify(schema))) + } }, { immediate: true } ) @@ -118,18 +69,6 @@ export default defineComponent({ } }, render(): any { - const { pageSchema }: { pageSchema: Schema } = this as any - - // 渲染画布增加根节点,与出码和预览保持一致 - const rootChildrenSchema: any = { - componentName: 'div', - // 把页级 props(主要是页面样式)挂到根容器 - props: { ...(pageSchema.props || {}) }, - children: pageSchema.children - } - - return this.pageSchema.children?.length - ? h(renderer, { schema: rootChildrenSchema, parent: this.pageSchema }) - : [h(Loading)] + return defaultRenderer(this.pageSchema as any) } }) diff --git a/packages/runtime-renderer/src/renderer/app-function/constant.ts b/packages/runtime-renderer/src/renderer/app-function/constant.ts new file mode 100644 index 000000000..c389b4e20 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/constant.ts @@ -0,0 +1,4 @@ +export const NODE_UID = 'data-uid' +export const NODE_TAG = 'data-tag' +export const NODE_LOOP = 'loop-id' +export const NODE_INACTIVE_UID = 'data-ia-uid' diff --git a/packages/runtime-renderer/src/renderer/app-function/dataSource/http.js b/packages/runtime-renderer/src/renderer/app-function/dataSource/http.js new file mode 100644 index 000000000..624fdd332 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/dataSource/http.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import Axios from 'axios' + +const config = { withCredentials: false } + +const axios = (config) => { + const instance = Axios.create(config) + const defaults = {} + return { + request(config) { + return instance(config) + }, + defaults(key, value) { + if (key && typeof key === 'string') { + if (typeof value === 'undefined') { + return instance.defaults[key] + } + instance.defaults[key] = value + defaults[key] = value + } else { + return instance.defaults + } + }, + defaultSettings() { + return defaults + }, + interceptors: { + request: { + use(fnHandle, fnError) { + return instance.interceptors.request.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.request.eject(id) + } + }, + response: { + use(fnHandle, fnError) { + return instance.interceptors.response.use(fnHandle, fnError) + }, + eject(id) { + return instance.interceptors.response.eject(id) + } + } + }, + CancelToken: Axios.CancelToken, + isCancel: Axios.isCancel + } +} + +export default ({ globalWillFetch, globalDataHandle, globalErrorHandler, willFetch, dataHandler, errorHandler }) => { + const http = axios(config) + // axios对于request拦截器是后注册先执行 + http.interceptors.request.use(willFetch, errorHandler) + http.interceptors.request.use(globalWillFetch, globalErrorHandler) + http.interceptors.response.use(dataHandler, errorHandler) + http.interceptors.response.use(globalDataHandle, globalErrorHandler) + return http +} diff --git a/packages/runtime-renderer/src/renderer/app-function/dataSource/index.ts b/packages/runtime-renderer/src/renderer/app-function/dataSource/index.ts new file mode 100644 index 000000000..6f1e941d1 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/dataSource/index.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import useHttp from './http.js' +const dataSourceMap: Record = {} + +const createFn = (fnStr: string) => { + return (...args: any) => { + const fn = new Function(`return ${fnStr}`)() + return fn.apply(this, args) + } +} + +export const initDataSource = (dataSources: any) => { + const globalWillFetch = dataSources.willFetch ? createFn(dataSources.willFetch.value) : (opt: any) => opt + const globalDataHandle = dataSources.dataHandler ? createFn(dataSources.dataHandler.value) : (res: any) => res + const globalErrorHandler = dataSources.errorHandler + ? createFn(dataSources.errorHandler.value) + : (err: any) => Promise.reject(err) + const execProxy = (config: any) => { + // TODO 通过全局配置代理, 通过代理服务器的方式获取接口数据,解决跨域问题 + const appId = new URLSearchParams(location.search).get('id') + const { proxy = {} } = dataSources || {} + if (proxy) { + const isProxy = Object.keys(proxy).reduce((acc, cur) => acc || config.url.startsWith(cur), false) + if (isProxy) { + config.url = `/proxy/api${config.url}` + config.headers = { ...config.headers, proxy_app_id: appId || 1 } + } + } + } + + const load = + (http: any, options: any, dataSource: any, shouldFetch: any) => (params: any, path: any, customConfig: any) => { + // 如果没有配置远程请求,则直接返回静态数据,返回前可能会有全局数据处理 + if (!options) { + return Promise.resolve(globalDataHandle(dataSource.config.data)) + } + + if (!shouldFetch()) { + return Promise.resolve(undefined) + } + + dataSource.status = 'loading' + + const { method, uri: url, params: defaultParams, timeout, headers } = options + const config = { method, url, headers, timeout, ...customConfig } + + const data = params || defaultParams + + config.url = path ? `${config.url}/${path}` : config.url + + execProxy(config) + + if (['get', 'delete'].includes(method.toLowerCase())) { + config.params = data + } else { + config.data = data + } + + return http.request(config) + } + + if (Array.isArray(dataSources.list)) { + dataSources.list?.forEach((conf: any) => { + const config = { name: conf.name, ...(conf.data || {}) } + const dataSource: any = { config } + dataSourceMap[config.name] = dataSource + const shouldFetch = config.shouldFetch?.value ? createFn(config.shouldFetch.value) : () => true + const willFetch = config.willFetch?.value ? createFn(config.willFetch.value) : (opt: any) => opt + const dataHandler = (res: any) => { + const data = config.dataHandler?.value ? createFn(config.dataHandler.value)(res) : res + dataSource.status = 'loaded' + dataSource.data = data + return data + } + const errorHandler = (error: any) => { + const err = config.errorHandler?.value ? createFn(config.errorHandler.value)(error) : error + dataSource.status = 'error' + dataSource.error = err + } + const http = useHttp({ + globalWillFetch, + globalDataHandle, + globalErrorHandler, + willFetch, + dataHandler, + errorHandler + }) + + dataSource.status = 'init' + dataSource.load = load(http, config.options, dataSource, shouldFetch) + }) + } +} + +export const getDataSource = () => dataSourceMap + +export default dataSourceMap diff --git a/packages/runtime-renderer/src/renderer/app-function/importMap.ts b/packages/runtime-renderer/src/renderer/app-function/importMap.ts new file mode 100644 index 000000000..c787e0a88 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/importMap.ts @@ -0,0 +1,161 @@ +import { importMapConfig } from '@opentiny/tiny-engine-common/js/importMap' +import config, { useEnv } from '../../../config.ts' + +const { + VITE_CDN_TYPE, + VITE_CDN_DOMAIN, + BASE_URL, + VITE_LOCAL_IMPORT_MAPS, + VITE_LOCAL_IMPORT_PATH = 'local-cdn-static' +} = useEnv() + +const getImportUrl = (pkgName: string) => { + // 自定义的 importMap + const customImportMap = config?.importMap as any + const sysImportMap = importMapConfig as any + const isLocalBundle = VITE_LOCAL_IMPORT_MAPS === 'true' + const versionDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/' : '@' + const fileDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/files' : '' + const cdnDomain = isLocalBundle ? BASE_URL + VITE_LOCAL_IMPORT_PATH : VITE_CDN_DOMAIN + + if (customImportMap?.imports?.[pkgName]) { + return customImportMap.imports[pkgName] + .replace('${VITE_CDN_DOMAIN}', cdnDomain) + .replace('${versionDelimiter}', versionDelimiter) + .replace('${fileDelimiter}', fileDelimiter) + } + + if (sysImportMap?.imports?.[pkgName]) { + return sysImportMap?.imports?.[pkgName] + .replace('${VITE_CDN_DOMAIN}', cdnDomain) + .replace('${versionDelimiter}', versionDelimiter) + .replace('${fileDelimiter}', fileDelimiter) + } +} + +// 获取样式文件的URL,后续去除物料内置逻辑之后,需要用户自行引入,相关逻辑也需要同步删除 +const getImportStyleUrl = (pkgName: string) => { + const isLocalBundle = VITE_LOCAL_IMPORT_MAPS === 'true' + const versionDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/' : '@' + const fileDelimiter = VITE_CDN_TYPE === 'npmmirror' && !isLocalBundle ? '/files' : '' + const cdnDomain = isLocalBundle ? BASE_URL + VITE_LOCAL_IMPORT_PATH : VITE_CDN_DOMAIN + const sysImportMap = importMapConfig as any + + if (sysImportMap.importStyles[pkgName]) { + return sysImportMap.importStyles[pkgName] + .replace('${VITE_CDN_DOMAIN}', cdnDomain) + .replace('${versionDelimiter}', versionDelimiter) + .replace('${fileDelimiter}', fileDelimiter) + } +} + +export function getImportMapData(canvasDeps = { scripts: [], styles: [] }) { + // 以下内容由于区块WebComponent加载需要补充 + const blockRequire = { + imports: { + // TODO: 后续版本发通知,不再内置物料,需要用户自行引入 + '@opentiny/vue': getImportUrl('@opentiny/vue'), + '@opentiny/vue-icon': getImportUrl('@opentiny/vue-icon'), + '@opentiny/tiny-engine-builtin-component': getImportUrl('@opentiny/tiny-engine-builtin-component') + }, + importStyles: [getImportStyleUrl('@opentiny/vue-theme')] + } + + // 以下内容由于物料协议不支持声明子依赖而@opentiny/vue需要依赖所以需要补充 + // TODO: 后续版本发通知,不再内置物料,需要用户自行引入 + const tinyVueRequire = { + imports: { + '@opentiny/vue-common': getImportUrl('@opentiny/vue-common'), + '@opentiny/vue-locale': getImportUrl('@opentiny/vue-locale'), + echarts: getImportUrl('echarts') + } + } + + const materialsAndUtilsRequire = canvasDeps.scripts.reduce((imports, { package: pkg, script }) => { + if (pkg && script) { + imports[pkg] = script + } + + return imports + }, {}) + + const importMap = { + imports: { + vue: getImportUrl('vue'), + 'vue-i18n': getImportUrl('vue-i18n'), + ...blockRequire.imports, + ...tinyVueRequire.imports, + ...materialsAndUtilsRequire + } + } + + const importStyles = [...blockRequire.importStyles, ...canvasDeps.styles] + const tailwindURL = getImportUrl('@tailwindcss/browser') + const importScripts = config?.enableTailwindCSS && tailwindURL ? [tailwindURL] : [] + + return { + importMap, + importStyles, + importScripts + } +} + +interface ITagProps { + tag: string + [key: string]: string +} + +export const IMPORT_MAP_ELEMENT_ID = 'tiny-engine-runtime-import-map' + +export function addTagTask(props: ITagProps) { + return new Promise((resolve, reject) => { + const { tag, onload, ...others } = props + let el: any = document.head.querySelector(`${tag}#${props.id}`) + const isCreate = !el + if (!el) { + el = document.createElement(tag) as any + } + for (const key in others) { + el[key] = others[key] as string + } + if (isCreate) { + document.head.appendChild(el) + } + const success = () => { + resolve(true) + // eslint-disable-next-line no-console + console.error('添加并加载脚本:', props) + } + if (onload) { + el.onload = success + el.onerror = reject + } else { + setTimeout(() => success()) + } + }) +} + +export async function initImportMap() { + const { importMap, importStyles, importScripts } = getImportMapData() + const tasks = [] + const task = addTagTask({ + id: IMPORT_MAP_ELEMENT_ID, + tag: 'script', + type: 'importmap', + textContent: JSON.stringify(importMap, null, 2) + }) + tasks.push(task) + importStyles.forEach((url) => { + const task = addTagTask({ + tag: 'link', + href: url, + type: config.enableTailwindCSS ? 'text/tailwindcss' : 'text/css' + }) + tasks.push(task) + }) + importScripts.forEach((url) => { + const task = addTagTask({ tag: 'script', type: 'module', src: url }) + tasks.push(task) + }) + await Promise.all(tasks) +} diff --git a/packages/runtime-renderer/src/renderer/app-function/index.ts b/packages/runtime-renderer/src/renderer/app-function/index.ts new file mode 100644 index 000000000..6ffc9cde0 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/index.ts @@ -0,0 +1,7 @@ +export * from './dataSource/index.ts' +export * from './loadCompLib.ts' +export * from './utils.ts' +export * from './importMap.ts' +export * from './store.ts' +export * from './router.ts' +export * from './constant.ts' diff --git a/packages/runtime-renderer/src/renderer/app-function/loadCompLib.ts b/packages/runtime-renderer/src/renderer/app-function/loadCompLib.ts new file mode 100644 index 000000000..659155759 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/app-function/loadCompLib.ts @@ -0,0 +1,63 @@ +// 定义组件配置接口 +interface ComponentConfig { + destructuring?: boolean + exportName?: string +} + +// 定义组件依赖接口 +interface ComponentDependency { + package?: string + script?: string + components: Record +} + +// 定义动态导入参数接口 +interface DynamicImportParams { + package: string + script?: string +} + +/** + * 动态导入获取组件库模块 + * @param {DynamicImportParams} param 模块参数,包含pkg模块名称和script模块的cdn地址 + * @returns {Promise} 返回组件库模块 + */ +const dynamicImportComponentLib = async ({ package: pkg, script }: DynamicImportParams): Promise => { + if (window.TinyComponentLibs[pkg]) { + return window.TinyComponentLibs[pkg] + } + try { + // 尝试直接导入模块 + const modules = await import(/* @vite-ignore */ pkg) + window.TinyComponentLibs[pkg] = modules + } catch (_err) { + if (script) { + try { + // 拉取远程脚本 + const modules = await import(/* @vite-ignore */ script) + window.TinyComponentLibs[pkg] = modules + } catch (error) { + // eslint-disable-next-line no-console + console.error(`组件库安装失败: ${pkg}`, error) + } + } + } + return window.TinyComponentLibs[pkg] +} + +/** + * 获取组件对象并缓存,组件渲染时使用 + * @param {ComponentDependency} param 组件的依赖配置对象 + * @returns {Promise} 无返回值的Promise + */ +export const getComponents = async ({ package: pkg, script, components }: ComponentDependency): Promise => { + if (!pkg) return + const modules = await dynamicImportComponentLib({ package: pkg, script }) + for (const i in components) { + const item = components[i] as any + if (!window.TinyLowcodeComponent[item.componentName || item.exportName]) { + window.TinyLowcodeComponent[item.componentName] = + item?.destructuring && item?.exportName ? modules[item.exportName] : modules?.default + } + } +} diff --git a/packages/runtime-renderer/src/router/index.ts b/packages/runtime-renderer/src/renderer/app-function/router.ts similarity index 86% rename from packages/runtime-renderer/src/router/index.ts rename to packages/runtime-renderer/src/renderer/app-function/router.ts index 40fbc1733..2594245cb 100644 --- a/packages/runtime-renderer/src/router/index.ts +++ b/packages/runtime-renderer/src/renderer/app-function/router.ts @@ -1,7 +1,8 @@ import { createRouter, createWebHashHistory } from 'vue-router' -import { useAppSchema } from '../composables/useAppSchema' -import type { IRouteConfig } from '../types/config' -import type { PageMeta } from '../types/schema' +import { useAppSchema } from '../../composables/useAppSchema.ts' +import type { IRouteConfig } from '../../types/index.ts' +import { withPageRenderer } from '../../components/PageRenderer.ts' +import type { PageMeta } from '../../types/index.ts' // 定义页面结构类型 interface PageSchema { @@ -17,9 +18,8 @@ interface PageSchema { } // 异步初始化路由配置 -async function createRouterConfig() { +function createRouterConfig() { const { pages } = useAppSchema() - // 通过pages生成路由配置 const generateRoutesByPages = (pages: Array): Array => { // 建立路由-页面id映射 @@ -31,8 +31,7 @@ async function createRouterConfig() { pageRouteMap.set(pageIdStr, { path: `${page.route}`, name: pageIdStr, - component: () => import('../components/PageRenderer.ts'), - props: { pageId: page.id }, + component: withPageRenderer({ pageId: page.id }), children: [], meta: { pageId: pageIdStr, @@ -85,14 +84,14 @@ async function createRouterConfig() { routes.push({ path: '/:pathMatch(.*)*', - component: () => import('../components/NotFound.vue') + component: () => import('../../components/NotFound.vue') }) return routes } -export async function createAppRouter() { - const routes = await createRouterConfig() +export function createAppRouter() { + const routes = createRouterConfig() const router = createRouter({ history: createWebHashHistory('/runtime.html'), routes diff --git a/packages/runtime-renderer/src/stores/index.ts b/packages/runtime-renderer/src/renderer/app-function/store.ts similarity index 80% rename from packages/runtime-renderer/src/stores/index.ts rename to packages/runtime-renderer/src/renderer/app-function/store.ts index 80f7a3d95..e54c1eb5c 100644 --- a/packages/runtime-renderer/src/stores/index.ts +++ b/packages/runtime-renderer/src/renderer/app-function/store.ts @@ -1,9 +1,8 @@ -import { defineStore, type Pinia } from 'pinia' +import { createPinia, defineStore } from 'pinia' import { shallowReactive } from 'vue' -import type { StoreConfig } from '../types/config' -import { useAppSchema } from '../composables/useAppSchema' -import { parseJSFunction } from '../utils/data-utils' - +import { useAppSchema } from '../../composables/useAppSchema' +import { parseJSFunction } from '../data-function' +const stores = shallowReactive>({}) export const generateStoresConfig = () => { const { globalStates } = useAppSchema() if (globalStates.value.length === 0) return [] @@ -37,9 +36,9 @@ export const generateStoresConfig = () => { })) } -export const createStores = (storesConfig: StoreConfig[], pinia: Pinia) => { - const stores = shallowReactive>({}) - +export const createAppStores = () => { + const pinia = createPinia() + const storesConfig = generateStoresConfig() storesConfig.forEach((config) => { // 使用 defineStore 创建 Pinia store const useStore = defineStore(config.id, { @@ -52,6 +51,9 @@ export const createStores = (storesConfig: StoreConfig[], pinia: Pinia) => { // 使用useStore创建 store 实例并绑定到 pinia stores[config.id] = useStore(pinia) }) + return pinia +} +export function getStore() { return stores } diff --git a/packages/runtime-renderer/src/app-function/utils.ts b/packages/runtime-renderer/src/renderer/app-function/utils.ts similarity index 95% rename from packages/runtime-renderer/src/app-function/utils.ts rename to packages/runtime-renderer/src/renderer/app-function/utils.ts index efac49418..19b6445c9 100644 --- a/packages/runtime-renderer/src/app-function/utils.ts +++ b/packages/runtime-renderer/src/renderer/app-function/utils.ts @@ -1,5 +1,5 @@ -import type { Util } from '../types/schema' -import { parseJSFunction } from '../utils/data-utils' +import type { Util } from '../../types/index.ts' +import { parseJSFunction } from '../data-function/index.ts' interface npmContent { package?: string diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue new file mode 100644 index 000000000..b4414c5cd --- /dev/null +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue index 53d64d83f..1f6363dc9 100644 --- a/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue +++ b/packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue @@ -1,5 +1,5 @@ diff --git a/packages/runtime-renderer/src/renderer/builtin/index.ts b/packages/runtime-renderer/src/renderer/builtin/index.ts index aa303aae9..66da676e6 100644 --- a/packages/runtime-renderer/src/renderer/builtin/index.ts +++ b/packages/runtime-renderer/src/renderer/builtin/index.ts @@ -9,23 +9,12 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -import CanvasText from './CanvasText.vue' -import CanvasIcon from './CanvasIcon.vue' -import CanvasSlot from './CanvasSlot.vue' -import CanvasImg from './CanvasImg.vue' -import CanvasPlaceholder from './CanvasPlaceholder.vue' -import CanvasRouterLink from './CanvasRouterLink.vue' -import CanvasRouterView from './CanvasRouterView.vue' -import CanvasCollection from './CanvasCollection.vue' - -export { - CanvasText, - CanvasIcon, - CanvasSlot, - CanvasImg, - CanvasPlaceholder, - CanvasRouterLink, - CanvasRouterView, - CanvasCollection -} +export { default as CanvasBox } from './CanvasBox.vue' +export { default as CanvasText } from './CanvasText.vue' +export { default as CanvasIcon } from './CanvasIcon.vue' +export { default as CanvasSlot } from './CanvasSlot.vue' +export { default as CanvasImg } from './CanvasImg.vue' +export { default as CanvasPlaceholder } from './CanvasPlaceholder.vue' +export { default as CanvasRouterLink } from './CanvasRouterLink.vue' +export { default as CanvasRouterView } from './CanvasRouterView.vue' +export { default as CanvasCollection } from './CanvasCollection.vue' diff --git a/packages/runtime-renderer/src/renderer/context/index.ts b/packages/runtime-renderer/src/renderer/context/index.ts new file mode 100644 index 000000000..e16a5aa06 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/index.ts @@ -0,0 +1,7 @@ +export * from './useContext' +export * from './useStore' +export * from './useMethods' +export * from './useRefs' +export * from './useState' +export * from './useDataSource' +export * from './useUtils' diff --git a/packages/runtime-renderer/src/renderer/context/useContext.ts b/packages/runtime-renderer/src/renderer/context/useContext.ts new file mode 100644 index 000000000..fc5490704 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useContext.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import * as vue from 'vue' +import { shallowReactive, type ShallowReactive } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { useStore } from './useStore.ts' +import { useUtils } from './useUtils.ts' +import { useRefs } from './useRefs.ts' +import { useState } from './useState.ts' +import { useMethods } from './useMethods.ts' +import { useDataSource } from './useDataSource.ts' +import { getDeletedKeys } from '../data-function/index.ts' +import { normalizeScopeKey, setPageCss } from '../page-function/index.ts' +import TinyI18nHost from '@opentiny/tiny-engine-common/js/i18n' +import type { PageContent as Schema } from '../../types/index.ts' + +interface Context { + [key: string]: any +} + +interface UseContextReturn { + context: ShallowReactive + setContext: (ctx: Context) => void + appendContext: (ctx: Context) => void + getContext: () => ShallowReactive +} + +export function useContext(): UseContextReturn { + const context = shallowReactive({}) + + const setContext = (ctx: Context) => { + const deletedKeys = getDeletedKeys(context, ctx) + deletedKeys?.forEach((key) => delete context[key]) + Object.assign(context, ctx) + } + + const appendContext = (ctx: Context) => { + setContext({ ...context, ...ctx }) + } + + const getContext = () => context + + return { + context, + setContext, + getContext, + appendContext + } +} + +interface InitContextProps { + schema: Schema + props: any + ctx: any + isBlock?: boolean +} + +export function useContextPage() { + const { context, setContext, appendContext } = useContext() + const route = useRoute() + const router = useRouter() + const { $, $ref } = useRefs() + const { stores } = useStore() + const { utils } = useUtils() + const { dataSourceMap } = useDataSource() + const { state, setState } = useState({}, context) + const { methods, setMethods } = useMethods({}, context) + const { t, locale } = TinyI18nHost.global + const initContext = ({ schema, props, isBlock, ctx }: InitContextProps, callback?: (...args: any[]) => void) => { + if (!schema) return + const cssScopeId = normalizeScopeKey(props.pageId, isBlock) + setContext({ + ...vue, + context: ctx, + t, + $, + $ref, + route, + router, + props, + state, + utils, + stores, + dataSourceMap, + i18n: { get: () => t }, + // setState: { get: () => setState }, + getLocale: { get: () => locale?.value }, + setLocale: { get: () => (val: string) => (locale.value = val) }, + location: { get: () => window.location }, + history: { get: () => window.history }, + getCssScopeId: () => cssScopeId + }) + setState(schema.state) + setMethods(schema.methods) + appendContext(methods) + setPageCss(schema.css || '', cssScopeId) + callback?.() + } + return { + state, + utils, + stores, + context, + methods, + initContext, + setContext, + appendContext, + setState, + setMethods, + $, + $ref + } +} diff --git a/packages/runtime-renderer/src/app-function/http/config.js b/packages/runtime-renderer/src/renderer/context/useDataSource.ts similarity index 75% rename from packages/runtime-renderer/src/app-function/http/config.js rename to packages/runtime-renderer/src/renderer/context/useDataSource.ts index cfa3714e1..3df8d9ce0 100644 --- a/packages/runtime-renderer/src/app-function/http/config.js +++ b/packages/runtime-renderer/src/renderer/context/useDataSource.ts @@ -9,7 +9,10 @@ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * */ - -export default { - withCredentials: false +import { getDataSource } from '../app-function' +export function useDataSource() { + return { + dataSourceMap: getDataSource() + } } +export { getDataSource } diff --git a/packages/runtime-renderer/src/renderer/context/useLowcode.ts b/packages/runtime-renderer/src/renderer/context/useLowcode.ts new file mode 100644 index 000000000..8d67287f4 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useLowcode.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2023 - present TinyEngine Authors. + * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { useRouter, useRoute } from 'vue-router' +import { getStore, getDataSource, getUtilsAll } from './index' +import { getCurrentInstance, nextTick, provide, inject } from 'vue' +import TinyI18nHost, { I18nInjectionKey } from '@opentiny/tiny-engine-common/js/i18n' + +export const lowcodeWrap = (props: any, context: any) => { + const global: any = {} + const instance = getCurrentInstance() as any + const router = useRouter() + const route = useRoute() + const i18nhost = inject(I18nInjectionKey) as any + const { t, locale } = i18nhost.global + const emit = context.emit + const ref = (ref: string) => instance?.refs?.[ref] + + const setState = (newState: any, callback: any) => { + Object.assign(global.state, newState) + nextTick(() => callback.apply(global)) + } + + const getLocale = () => locale.value + const setLocale = (val: string) => { + locale.value = val + } + + const location = () => window.location + const history = () => window.history + + Object.defineProperties(global, { + i18n: { get: () => t }, + emit: { get: () => emit }, + props: { get: () => props }, + route: { get: () => route }, + router: { get: () => router }, + setState: { get: () => setState }, + getLocale: { get: () => getLocale }, + setLocale: { get: () => setLocale }, + utils: { get: () => getUtilsAll() }, + dataSourceMap: { get: () => getDataSource() }, + location: { get: location }, + history: { get: history }, + bridge: { get: () => {} }, + $: { get: () => ref } + }) + + const wrap = (fn: any) => { + if (typeof fn === 'function') { + return (...args: any[]) => fn.apply(global, args) + } + + Object.entries(fn).forEach(([name, value]) => { + Object.defineProperty(global, name, { + get: () => value + }) + }) + + fn.t = t + + return fn + } + + return wrap +} + +export const lowcode = () => { + const i18n = inject(I18nInjectionKey) as any + + provide(I18nInjectionKey, i18n) + + return { t: i18n.global.t, lowcodeWrap, stores: getStore() } +} + +export const useLowcode = () => { + const i18nHost = TinyI18nHost as any + i18nHost.lowcode = lowcode + return { + TinyI18nHost: i18nHost + } +} diff --git a/packages/runtime-renderer/src/renderer/context/useMethods.ts b/packages/runtime-renderer/src/renderer/context/useMethods.ts new file mode 100644 index 000000000..43a2d71a9 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useMethods.ts @@ -0,0 +1,22 @@ +import { shallowReactive } from 'vue' +import { parseData } from '../data-function/index' +import type { IFuntion } from '../../types/index' +export function useMethods(scope: any = {}, context: any = {}) { + const methods = shallowReactive>({}) + const setMethods = (methodsObj: Record) => { + for (const key in methodsObj) { + const method = methodsObj[key] + methods[key] = parseData(method, scope, context) as IFuntion + } + } + const delMethods = (key: string) => { + delete methods[key] + } + const getMethods = () => methods + return { + methods, + setMethods, + delMethods, + getMethods + } +} diff --git a/packages/runtime-renderer/src/renderer/context/useRefs.ts b/packages/runtime-renderer/src/renderer/context/useRefs.ts new file mode 100644 index 000000000..86af6ebb7 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useRefs.ts @@ -0,0 +1,10 @@ +import { shallowReactive } from 'vue' +export function useRefs() { + const refsMap = shallowReactive>({}) + return { + $: (refName: string) => refsMap[refName], + $ref: (refName: string, value: any) => { + refsMap[refName] = value + } + } +} diff --git a/packages/runtime-renderer/src/renderer/context/useState.ts b/packages/runtime-renderer/src/renderer/context/useState.ts new file mode 100644 index 000000000..efa2edc09 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useState.ts @@ -0,0 +1,37 @@ +import { reactive } from 'vue' +import { getDeletedKeys } from '../data-function' +import { useAccessorMap } from '../page-function/accessor' +import { isStateAccessor, parseData } from '../data-function/index' + +export function useState(scope: any = {}, context: any = {}) { + // 改成使用 reactive, 处理state.xxx.xxx双向绑定 + const state = reactive>({}) + const { generateStateAccessors } = useAccessorMap(context) + + const setState = (data: Record) => { + if (typeof data !== 'object' || data === null) { + return + } + // 同步删除的 key + const deletedKeys = getDeletedKeys(state, data) + deletedKeys?.forEach((key) => delete state[key]) + Object.assign(state, parseData(data, scope, context) || {}) + // 在状态变量合并之后,执行访问器中watchEffect,为了可以在访问器函数中可以访问其他state变量 + Object.entries(data || {})?.forEach(([key, stateData]: [string, any]) => { + if (isStateAccessor(stateData)) { + const accessor = stateData.accessor + if (accessor?.getter?.value) { + generateStateAccessors('getter', accessor, key) + } + + if (accessor?.setter?.value) { + generateStateAccessors('setter', accessor, key) + } + } + }) + } + return { + state, + setState + } +} diff --git a/packages/runtime-renderer/src/renderer/context/useStore.ts b/packages/runtime-renderer/src/renderer/context/useStore.ts new file mode 100644 index 000000000..bbedfd78c --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useStore.ts @@ -0,0 +1,8 @@ +import { getStore } from '../app-function' + +export function useStore() { + return { + stores: getStore() + } +} +export { getStore } diff --git a/packages/runtime-renderer/src/renderer/context/useUtils.ts b/packages/runtime-renderer/src/renderer/context/useUtils.ts new file mode 100644 index 000000000..111c31b17 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/context/useUtils.ts @@ -0,0 +1,9 @@ +import { getUtilsAll } from '../app-function' + +export function useUtils() { + return { + utils: getUtilsAll() + } +} + +export { getUtilsAll } diff --git a/packages/runtime-renderer/src/renderer/parser/index.ts b/packages/runtime-renderer/src/renderer/data-function/index.ts similarity index 51% rename from packages/runtime-renderer/src/renderer/parser/index.ts rename to packages/runtime-renderer/src/renderer/data-function/index.ts index e9312eb33..c976eb70b 100644 --- a/packages/runtime-renderer/src/renderer/parser/index.ts +++ b/packages/runtime-renderer/src/renderer/data-function/index.ts @@ -1 +1,2 @@ export * from './parser' +export * from './utils' diff --git a/packages/runtime-renderer/src/renderer/parser/parser.ts b/packages/runtime-renderer/src/renderer/data-function/parser.ts similarity index 53% rename from packages/runtime-renderer/src/renderer/parser/parser.ts rename to packages/runtime-renderer/src/renderer/data-function/parser.ts index c5126258d..9755f710d 100644 --- a/packages/runtime-renderer/src/renderer/parser/parser.ts +++ b/packages/runtime-renderer/src/renderer/data-function/parser.ts @@ -10,70 +10,69 @@ * */ +import * as vue from 'vue' import babelPluginJSX from '@vue/babel-plugin-jsx' import { transformSync } from '@babel/core' import { Notify } from '@opentiny/vue' -import i18nHost from '@opentiny/tiny-engine-i18n-host' import { renderDefault } from '../render' +import { getComponent, getIcon } from '../material-function' +import i18nHost from '@opentiny/tiny-engine-common/js/i18n' interface ITypeParserDef { - type: (data) => boolean + type: (data: any) => boolean parseFunc: (data: unknown, scope: Record, ctx: Record) => unknown } const parseList: Array = [] -const isI18nData = (data) => { +const isI18nData = (data: { type: string }) => { return data && data.type === 'i18n' } -const isJSSlot = (data) => { +const isJSSlot = (data: { type: string }) => { return data && data.type === 'JSSlot' } -const isJSExpression = (data) => { +const isJSExpression = (data: { type: string }) => { return data && data.type === 'JSExpression' } -const isJSFunction = (data) => { +const isJSFunction = (data: { type: string }) => { return data && data.type === 'JSFunction' } -const isJSResource = (data) => { +const isJSResource = (data: { type: string }) => { return data && data.type === 'JSResource' } -const isString = (data) => { +const isString = (data: any) => { return typeof data === 'string' } -const isArray = (data) => { +const isArray = (data: any) => { return Array.isArray(data) } -const isFunction = (data) => { +const isFunction = (data: any) => { return typeof data === 'function' } -const isIcon = (data) => { +const isIcon = (data: { componentName: string }) => { return data?.componentName === 'Icon' } -const isObject = (data) => { +const isObject = (data: any) => { return typeof data === 'object' } // 判断是否是状态访问器 -export const isStateAccessor = (stateData) => +export const isStateAccessor = (stateData: { accessor: { getter: { type: string }; setter: { type: string } } }) => stateData?.accessor?.getter?.type === 'JSFunction' || stateData?.accessor?.setter?.type === 'JSFunction' // 规避创建function eslint报错 -export const newFn = (...argv) => { - const Fn = Function - return new Fn(...argv) -} +export const newFn = (...args: any) => new Function(...args) -const transformJSX = (code) => { +const transformJSX = (code: any) => { const res = transformSync(code, { plugins: [ [ @@ -84,7 +83,7 @@ const transformJSX = (code) => { ] ] }) - return (res.code || '') + return (res?.code || '') .replace(/import \{.+\} from "vue";/, '') .replace(/h\(_?resolveComponent\((.*?)\)/g, `h(this.getComponent($1)`) .replace(/_?resolveComponent/g, 'h') @@ -92,7 +91,11 @@ const transformJSX = (code) => { .trim() } -const parseExpression = (data, scope, ctx, isJsx = false) => { +const curriedFn = (innerFn: any, params: any) => { + return (...args: any[]) => innerFn(...args, ...params) +} + +const parseExpression = (data: any, scope: any, ctx: any, isJsx = false) => { try { if (data.value.indexOf('this.i18n') > -1) { ctx.i18n = i18nHost.global.t @@ -100,22 +103,27 @@ const parseExpression = (data, scope, ctx, isJsx = false) => { ctx.t = i18nHost.global.t } + const fnContext = { ...ctx, ...scope, slotScope: scope } const expression = isJsx ? transformJSX(data.value) : data.value - return newFn('$scope', `with($scope || {}) { return ${expression} }`).call(ctx, { - ...ctx, - ...scope, - slotScope: scope - }) + const rs = newFn('$scope', `with($scope || {}) { return ${expression} }`).call(ctx, fnContext) + if (data.params && data.params.length) { + const params = data.params.map((param: string) => fnContext[param]) + return curriedFn(rs, params) + } else { + return rs + } } catch (err) { // 解析抛出异常,则再尝试解析 JSX 语法。如果解析 JSX 语法仍然出现错误,isJsx 变量会确保不会再次递归执行解析 if (!isJsx) { return parseExpression(data, scope, ctx, true) } + // eslint-disable-next-line no-console + console.error('parseExpression error', data, scope) return undefined } } -const parseI18n = (i18n, scope, ctx) => { +const parseI18n = (i18n: any, scope: any, ctx: any) => { return parseExpression( { type: 'JSExpression', @@ -127,7 +135,7 @@ const parseI18n = (i18n, scope, ctx) => { } // 解析函数字符串结构 -const parseFunctionString = (fnStr) => { +const parseFunctionString = (fnStr: string) => { const fnRegexp = /(async)?.*?(\w+) *\(([\s\S]*?)\) *\{([\s\S]*)\}/ const result = fnRegexp.exec(fnStr) if (result) { @@ -145,7 +153,7 @@ const parseFunctionString = (fnStr) => { } // 解析JSX字符串为可执行函数 -const parseJSXFunction = (data, _scope, ctx) => { +const parseJSXFunction = (data: any, _scope: null, ctx: any) => { try { const newValue = transformJSX(data.value) const fnInfo = parseFunctionString(newValue) @@ -153,7 +161,7 @@ const parseJSXFunction = (data, _scope, ctx) => { return newFn(...fnInfo.params, fnInfo.body).bind({ ...ctx, - getComponent: ctx.getComponent + getComponent }) } catch (error) { Notify({ @@ -166,91 +174,68 @@ const parseJSXFunction = (data, _scope, ctx) => { } } -export const generateFn = (innerFn, context) => { - return (...args) => { +export const generateFn = (innerFn: any, context: any) => { + return (...args: any[]) => { // 如果有数据源标识,则表格的fetchData返回数据源的静态数据 - const sourceId = context?.collectionMethodsMap?.[innerFn.realName || innerFn.name] - if (sourceId) { - return innerFn.call(context, ...args) - } else { - let result = null - - // 这里是为了兼容用户写法报错导致画布异常,但无法捕获promise内部的异常 - try { - result = innerFn.call(context, ...args) - } catch (error) { - Notify({ - type: 'warning', - title: `函数:${innerFn.name}执行报错`, - message: error?.message || `函数:${innerFn.name}执行报错,请检查语法` - }) - } - - // 这里注意如果innerFn返回的是一个promise则需要捕获异常,重新返回默认一条空数据 - if (result?.then) { - result = new Promise((resolve) => { - result.then(resolve).catch((error) => { - Notify({ - type: 'warning', - title: '异步函数执行报错', - message: error?.message || '异步函数执行报错,请检查语法' - }) - // 这里需要至少返回一条空数据,方便用户使用表格默认插槽 - resolve({ - result: [{}], - page: { total: 1 } - }) + let result: any + // 这里是为了兼容用户写法报错导致画布异常,但无法捕获promise内部的异常 + try { + result = innerFn.call(context, ...args) + } catch (error) { + Notify({ + type: 'warning', + title: `函数:${innerFn.name}执行报错`, + message: error?.message || `函数:${innerFn.name}执行报错,请检查语法` + }) + } + // 这里注意如果innerFn返回的是一个promise则需要捕获异常,重新返回默认一条空数据 + if (result?.then && typeof result.then === 'function') { + result = new Promise((resolve) => { + result.then(resolve).catch((error: { message: any }) => { + Notify({ + type: 'warning', + title: '异步函数执行报错', + message: error?.message || '异步函数执行报错,请检查语法' + }) + // 这里需要至少返回一条空数据,方便用户使用表格默认插槽 + resolve({ + result: [{}], + page: { total: 1 } }) }) - } - - return result + }) } + return result } } -const parseJSFunction = (data, _scope, ctx) => { +const parseJSFunction = (data: any, _scope: any, ctx: any) => { try { - const innerFn = newFn(`return ${data.value}`).bind(ctx)() + const innerFn = newFn('$vue', `with($vue || {}) { return ${data.value} }`).call(ctx, { vue }) return generateFn(innerFn, ctx) } catch (error) { return parseJSXFunction(data, null, ctx) } } -const parseJSSlot = (data, _scope, _ctx) => { - return ($scope) => renderDefault(data.value, { ..._scope, ...$scope }, data) -} -export function parseData(data, scope, ctx) { - const typeParser = parseList.find((item) => item.type(data)) - return typeParser ? typeParser.parseFunc(data, scope, ctx) : data -} - -export const parseCondition = (condition, scope, ctx) => { - // eslint-disable-next-line no-eq-null - return condition == null ? true : parseData(condition, scope, ctx) +const parseJSSlot = (data: any, _scope: Record, _ctx: any) => { + return ($scope: Record) => renderDefault(data.value, { ..._scope, ...$scope }, data) } -export const parseLoopArgs = (loop?: { item: unknown; index: number; loopArgs?: string[] }) => { - if (!loop) { - return undefined - } - const { item, index, loopArgs = [] } = loop - const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }` - return newFn('item,index', body)(item, index) +const parseIcon = (data: any, _scope: any, _ctx: any) => { + return getIcon(data.props.name) } -const getIcon = (name) => window.TinyVueIcon?.[name]?.() || '' - -const parseIcon = (data, _scope, _ctx) => { - return getIcon(data.props.name) +const parseData = (data: any, scope: any, ctx: any) => { + const typeParser = parseList.find((item) => item.type(data)) + return typeParser ? typeParser.parseFunc(data, scope, ctx) : data } -const parseStateAccessor = (data, _scope, ctx) => { +const parseStateAccessor = (data: any, _scope: any, ctx: any) => { return parseData(data.defaultValue, null, ctx) } -const parseObjectData = (data, scope, ctx) => { +const parseObjectData = (data: any, scope: any, ctx: any) => { if (!data) { return data } @@ -265,11 +250,14 @@ const parseObjectData = (data, scope, ctx) => { return getIcon(data.props.name) } - const res = {} + const res: any = {} Object.entries(data).forEach(([key, value]: [string, any]) => { // 如果是插槽则需要进行特殊处理 if (key === 'slot' && value?.name) { res[key] = value.name + // 特殊处理下ref + } else if (key === 'ref' && value) { + res[key] = (el: any) => ctx?.$ref(value, el) } else { res[key] = parseData(value, scope, ctx) } @@ -297,63 +285,77 @@ const parseObjectData = (data, scope, ctx) => { return res } -const parseString = (data) => { +const parseString = (data: any) => { return data.trim() } -const parseArray = (data, scope, ctx) => { - return data.map((item) => parseData(item, scope, ctx)) +const parseArray = (data: any, scope: any, ctx: any) => { + return data.map((item: any) => parseData(item, scope, ctx)) } -const parseFunction = (data, scope, ctx) => { +const parseFunction = (data: any, _scope: any, ctx: any) => { return data.bind(ctx) } +const parseCondition = (condition: any, scope: any, ctx: any) => { + // eslint-disable-next-line no-eq-null + return condition == null ? true : parseData(condition, scope, ctx) +} + +const parseLoopArgs = (loop?: { item: unknown; index: number; loopArgs?: string[] }) => { + if (!loop) { + return undefined + } + const { item, index, loopArgs = [] } = loop + const body = `return {${loopArgs[0] || 'item'}: item, ${loopArgs[1] || 'index'} : index }` + return newFn('item, index', body)(item, index) +} + parseList.push( - ...[ - { - type: isJSExpression, - parseFunc: parseExpression - }, - { - type: isI18nData, - parseFunc: parseI18n - }, - { - type: isJSFunction, - parseFunc: parseJSFunction - }, - { - type: isJSResource, - parseFunc: parseExpression - }, - { - type: isJSSlot, - parseFunc: parseJSSlot - }, - { - type: isIcon, - parseFunc: parseIcon - }, - { - type: isStateAccessor, - parseFunc: parseStateAccessor - }, - { - type: isString, - parseFunc: parseString - }, - { - type: isArray, - parseFunc: parseArray - }, - { - type: isFunction, - parseFunc: parseFunction - }, - { - type: isObject, - parseFunc: parseObjectData - } - ] + { + type: isJSExpression, + parseFunc: parseExpression + }, + { + type: isI18nData, + parseFunc: parseI18n + }, + { + type: isJSFunction, + parseFunc: parseJSFunction + }, + { + type: isJSResource, + parseFunc: parseExpression + }, + { + type: isJSSlot, + parseFunc: parseJSSlot + }, + { + type: isIcon, + parseFunc: parseIcon + }, + { + type: isStateAccessor, + parseFunc: parseStateAccessor + }, + { + type: isString, + parseFunc: parseString + }, + { + type: isArray, + parseFunc: parseArray + }, + { + type: isFunction, + parseFunc: parseFunction + }, + { + type: isObject, + parseFunc: parseObjectData + } ) + +export { parseData, parseCondition, parseLoopArgs } diff --git a/packages/runtime-renderer/src/utils/data-utils.ts b/packages/runtime-renderer/src/renderer/data-function/utils.ts similarity index 79% rename from packages/runtime-renderer/src/utils/data-utils.ts rename to packages/runtime-renderer/src/renderer/data-function/utils.ts index fdccb2a6f..5b575b681 100644 --- a/packages/runtime-renderer/src/utils/data-utils.ts +++ b/packages/runtime-renderer/src/renderer/data-function/utils.ts @@ -1,8 +1,7 @@ -const fun_ctor = Function - +import { newFn } from './parser' export function generateFunction(rawCode: any, context = {}) { try { - return fun_ctor(`return (${rawCode})`).call(context).bind(context) + return newFn(`return (${rawCode})`).call(context).bind(context) } catch (error) { // eslint-disable-next-line no-console console.error(`generateFunction error: ${JSON.stringify(error)}`) @@ -13,12 +12,6 @@ export const reset = (obj) => { Object.keys(obj).forEach((key) => delete obj[key]) } -// 规避创建function eslint报错 -export const newFn = (...argv) => { - const Fn = Function - return new Fn(...argv) -} - // 用于解析store中的actions和getters export const parseJSFunction = (data: any, _scope: any = null, _ctx: any = null) => { try { diff --git a/packages/runtime-renderer/src/renderer/material-function/blockComplier.ts b/packages/runtime-renderer/src/renderer/material-function/blockComplier.ts new file mode 100644 index 000000000..5fed97ed0 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/material-function/blockComplier.ts @@ -0,0 +1,33 @@ +import { compile as blockCompiler } from '@opentiny/tiny-engine-block-compiler' +import { genSFCWithDefaultPlugin } from '@opentiny/tiny-engine-dsl-vue' +import { useAppSchema } from '../../composables/useAppSchema' +const blockCompileCache = new Map() +export const getBlockCompileResult = async (name: any) => { + if (blockCompileCache.has(name)) { + return { + [name]: blockCompileCache.get(name) + } + } + + const list: any = await useAppSchema().fetchBlockByName(name) + + const block = list?.histories?.find((item: any) => item.version === list?.version) + + const realSchema = block.content || list?.content + if (!realSchema) { + return + } + const componentsMap = useAppSchema().appSchema.value?.componentsMap || [] + + // 需要出码的区块 + const sourceCode = genSFCWithDefaultPlugin(realSchema, componentsMap || [], { blockRelativePath: './' }) + + const blocksSourceCode = { + fileName: realSchema.fileName, + sourceCode + } + + const compiledResult = blockCompiler([blocksSourceCode], { compileCache: blockCompileCache }) + + return compiledResult +} diff --git a/packages/runtime-renderer/src/renderer/material-function/index.ts b/packages/runtime-renderer/src/renderer/material-function/index.ts new file mode 100644 index 000000000..c7cb98be1 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/material-function/index.ts @@ -0,0 +1,2 @@ +export * from './material-getter' +export * from './blockComplier' diff --git a/packages/runtime-renderer/src/renderer/material-function/material-getter.ts b/packages/runtime-renderer/src/renderer/material-function/material-getter.ts new file mode 100644 index 000000000..189fca853 --- /dev/null +++ b/packages/runtime-renderer/src/renderer/material-function/material-getter.ts @@ -0,0 +1,122 @@ +import { h, defineAsyncComponent, reactive, defineComponent } from 'vue' +import { isHTMLTag } from '@vue/shared' +import { + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasFlexBox, + CanvasSection, + CanvasNavigation, + FormModel, + TableModel, + PageModel +} from '@opentiny/tiny-engine-builtin-component' +import BlockLoadError from '../../components/BlockLoadError.vue' +import { + CanvasBox, + CanvasText, + CanvasIcon, + CanvasSlot, + CanvasImg, + CanvasPlaceholder, + CanvasRouterLink, + CanvasRouterView +} from '../builtin' +import { getBlockCompileResult } from './blockComplier' +import { addTagTask } from '../app-function/importMap' +import config from '../../../config.ts' + +export const Mapper: any = { + Icon: CanvasIcon, + Text: CanvasText, + div: CanvasBox, + Slot: CanvasSlot, + slot: CanvasSlot, + Template: CanvasBox, + Img: CanvasImg, + CanvasSection, + CanvasFlexBox, + CanvasRow, + CanvasCol, + CanvasRowColContainer, + CanvasPlaceholder, + FormModel, + TableModel, + PageModel, + RouterView: CanvasRouterView, + RouterLink: CanvasRouterLink, + CanvasNavigation +} +const getNative = (name: string) => { + return window.TinyLowcodeComponent?.[name] +} + +const getBlock = (name: string) => { + return window.blocks?.[name] +} + +const blockComponentsBlobUrlMap = new Map() + +// TODO: 这里的全局 getter 方法名,可以做成配置化 +const loadBlockComponent = async (name: string) => { + try { + if (blockComponentsBlobUrlMap.has(name)) { + return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name)) + } + + const blocksBlob = (await getBlockCompileResult(name)) as Array<{ blobURL: string; style: string }> + + for (const [fileName, value] of Object.entries(blocksBlob)) { + blockComponentsBlobUrlMap.set(fileName, value.blobURL) + + if (!value.style) { + continue + } + + // 注册 JS,以区块为维度 + addTagTask({ + id: fileName, + tag: 'style', + textContent: value.style, + type: config.enableTailwindCSS ? 'text/tailwindcss' : 'text/css' + }) + } + + return import(/* @vite-ignore */ blockComponentsBlobUrlMap.get(name)) + } catch (error) { + // 加载错误提示 + return h(BlockLoadError, { name }) + } +} + +window.loadBlockComponent = loadBlockComponent + +export const getBlockComponent = (name: string) => { + return defineAsyncComponent(() => loadBlockComponent(name)) +} + +// 移除区块缓存 +export const removeBlockCompsCache = () => { + blockComponentsBlobUrlMap.forEach((_, fileName) => { + const stylesheet = document.querySelector(`#${fileName}`) + stylesheet?.remove?.() + }) + + blockComponentsBlobUrlMap.clear() +} + +// 获取图标组件 +export const getIcon = (name: string) => { + return defineComponent({ + name: 'Icon', + render() { + return h(CanvasIcon, { name, ...this.$props }) + } + }) +} + +export const getComponent = (name: string) => { + return Mapper[name] || getNative(name) || getBlock(name) || (isHTMLTag(name) ? name : getBlockComponent(name)) +} + +export const blockSlotDataMap = reactive>({}) diff --git a/packages/runtime-renderer/src/renderer/page-function/accessor.ts b/packages/runtime-renderer/src/renderer/page-function/accessor.ts index 2cb4ed3ba..ee1da1c92 100644 --- a/packages/runtime-renderer/src/renderer/page-function/accessor.ts +++ b/packages/runtime-renderer/src/renderer/page-function/accessor.ts @@ -1,5 +1,5 @@ import { watchEffect, type WatchStopHandle } from 'vue' -import { generateFunction } from '../../utils/data-utils' +import { generateFunction } from '../data-function' type IAccessorType = 'getter' | 'setter' interface IAccessor { diff --git a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts b/packages/runtime-renderer/src/renderer/page-function/blockContext.ts deleted file mode 100644 index a32cc0c39..000000000 --- a/packages/runtime-renderer/src/renderer/page-function/blockContext.ts +++ /dev/null @@ -1,92 +0,0 @@ -// @ts-nocheck -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -import { nextTick, inject } from 'vue' -import { getCSSHandler } from './css.ts' -import { parseData } from '../parser/parser.ts' -import { useState } from './state.ts' -import useContext from '../useContext.ts' -import type { PageContent as Schema } from '../../types/schema.ts' -import dataSourceMap from '../../app-function/dataSource.js' -import { getUtilsAll } from '../../app-function/utils.ts' - -const invalidateCharRE = /[^a-z0-9-]/g - -export const getBlockCssScopeId = (fileName?: string) => { - const normalized = String(fileName || 'unknown') - .toLowerCase() - .replace(invalidateCharRE, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - return `data-te-page-block-${normalized || 'unknown'}` -} - -// 创建 context 实例的工厂函数 -export const createBlockContext = () => { - const contextApi = useContext() as any - const { context, setContext, getContext } = contextApi - const stores = inject('stores') - const methods: Record = {} - const { state, setState } = useState({ getContext }) - - const setMethods = (data: Record = {}, clear?: boolean) => { - if (clear) { - Object.keys(methods).forEach((key) => delete methods[key]) - } - Object.assign( - methods, - Object.fromEntries( - Object.keys(data).map((key) => { - return [key, parseData(data[key], {}, getContext())] - }) - ) - ) - setContext(methods) - } - - const setSchema = async (data: Schema) => { - if (!data) return - - const newSchema = JSON.parse(JSON.stringify(data)) - - const cssScopeId = getBlockCssScopeId(data.fileName) - const contextData = { - state, - stores, - dataSourceMap, - utils: getUtilsAll(), - cssScopeId, - getCssScopeId: () => cssScopeId - } - setContext(contextData, true) - setMethods(newSchema.methods, true) - setState(newSchema.state, true) - await nextTick() - - const cssHandler = getCSSHandler({ enableScoped: true }) - cssHandler.setPageCss(data.css || '', cssScopeId) - return context - } - - return { - setSchema, - getContext: () => context - } -} - -// 暂时不写成异步函数形式,方便后续调用 -export const getBlockContext = (schema: Schema): Record => { - const blockContext = createBlockContext() - blockContext.setSchema(schema) - return blockContext.getContext() -} diff --git a/packages/runtime-renderer/src/renderer/page-function/css.ts b/packages/runtime-renderer/src/renderer/page-function/css.ts index 51ff20345..a94ac6a28 100644 --- a/packages/runtime-renderer/src/renderer/page-function/css.ts +++ b/packages/runtime-renderer/src/renderer/page-function/css.ts @@ -12,141 +12,72 @@ import postcss from 'postcss' import scopedPlugin from './scope-css-plugin' - -interface CSSHandlerOptions { - pageId?: string - enableScoped?: boolean - enableModernCSS?: boolean -} - -type AdoptedSheets = CSSStyleSheet[] | undefined - -const supportsAdoptedStyleSheet = - typeof document !== 'undefined' && Array.isArray((document as any)?.adoptedStyleSheets) -const styleSheetMap = new Map() -const fallbackStyleMap = new Map() -let enableScoped = true - -function normalizeScopeKey(pageId?: string): string { - if (!pageId) { - return 'data-te-page-default' - } - return pageId.startsWith('data-te-page-') ? pageId : `data-te-page-${pageId}` -} - -function ensureAdoptedStyleSheet(key: string): CSSStyleSheet { - let sheet = styleSheetMap.get(key) - if (!sheet) { - sheet = new CSSStyleSheet() - styleSheetMap.set(key, sheet) - const adoptedSheets = ((document as any).adoptedStyleSheets || []) as AdoptedSheets - if (!adoptedSheets?.includes(sheet)) { - ;(document as any).adoptedStyleSheets = [...(adoptedSheets || []), sheet] - } - } - return sheet +import config from '../../../config.ts' + +export function getBlockCssScopeId(fileName?: string): string { + const invalidateCharRE = /[^a-z0-9-]/g + const normalized = String(fileName || 'default') + .toLowerCase() + .replace(invalidateCharRE, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + return `data-render-block-${normalized}` } -function ensureFallbackStyleElement(key: string): HTMLStyleElement { - let styleElement = fallbackStyleMap.get(key) - if (!styleElement) { - styleElement = document.createElement('style') - styleElement.type = 'text/css' - styleElement.setAttribute('data-te-page', key) - document.head?.appendChild(styleElement) - fallbackStyleMap.set(key, styleElement) +export function normalizeScopeKey(pageId?: string, isBlock?: boolean): string { + const id = String(pageId) + if (!id) { + return 'data-render-page-default' + } else if (id.startsWith('data-render-')) { + return id + } else if (isBlock) { + return getBlockCssScopeId(id) + } else { + return `data-render-page-${id}` } - return styleElement } -function processScopedCss(key: string, css: string): Promise { - if (!enableScoped) { - return Promise.resolve(css) - } - - return postcss([scopedPlugin(key)]) - .process(css, { from: undefined }) - .then((result) => result.css) +export function handleScopedCss(id: string, content: string) { + const plugins = id ? [scopedPlugin(id)] : [] + return postcss(plugins).process(content, { from: undefined }) } -function applyCss(key: string, css: string): void { - if (supportsAdoptedStyleSheet && typeof CSSStyleSheet !== 'undefined') { - const sheet = ensureAdoptedStyleSheet(key) - processScopedCss(key, css) - .then((scopedCss) => { - sheet.replaceSync(scopedCss) - }) - .catch(() => { - sheet.replaceSync(css) - }) - return - } - - const styleElement = ensureFallbackStyleElement(key) - processScopedCss(key, css) - .then((scopedCss) => { - styleElement.textContent = scopedCss - }) - .catch(() => { - styleElement.textContent = css - }) -} +export function addStyle(key: string, content: string) { + if (!content) return + let styleSheet = document.querySelector(`#${key}`) -function removePageCss(key: string): void { - const sheet = styleSheetMap.get(key) - if (sheet) { - styleSheetMap.delete(key) - const adoptedSheets = ((document as any).adoptedStyleSheets || []) as AdoptedSheets - if (adoptedSheets?.length) { - ;(document as any).adoptedStyleSheets = adoptedSheets.filter((item) => item !== sheet) + if (!styleSheet) { + styleSheet = document.createElement('style') + styleSheet.setAttribute('id', key) + if (config.enableTailwindCSS) { + styleSheet.setAttribute('type', 'text/tailwindcss') } + document.head.appendChild(styleSheet) } - - const styleElement = fallbackStyleMap.get(key) - if (styleElement?.parentNode) { - styleElement.parentNode.removeChild(styleElement) - fallbackStyleMap.delete(key) - } + const id = { [key]: key, 'app-global-css': '' }[key] + handleScopedCss(id, content).then((scopedCss) => { + styleSheet.textContent = scopedCss.css + }) } - -function clearAllStyles(): void { - styleSheetMap.forEach((_, key) => removePageCss(key)) - fallbackStyleMap.forEach((_, key) => removePageCss(key)) -} - export function setPageCss(css: string = '', pageId?: string): void { - const key = normalizeScopeKey(pageId) - - if (!css) { - removePageCss(key) - return - } - //console.log('setPageCss key', key, css) - - applyCss(key, css) -} - -export function clearAllPageCSS(): void { - clearAllStyles() + addStyle(normalizeScopeKey(pageId), css) } -export function getCSSHandler(options?: CSSHandlerOptions): { - setPageCss: (css?: string, pageId?: string) => void - clearAllStyles: () => void - removePageCss: (key: string) => void -} { - if (options?.enableScoped !== undefined) { - enableScoped = options.enableScoped - } - - return { - setPageCss, - clearAllStyles, - removePageCss: (key: string) => removePageCss(normalizeScopeKey(key)) +function clearPageCss(key: string): void { + const styleSheet = document.querySelector(`#${key}`) + if (styleSheet) { + styleSheet.remove() } } +function clearAllPageCSS(): void { + const styleSheets = document.head.querySelectorAll('[id^="data-render-page-"]') + styleSheets?.forEach((styleSheet) => { + styleSheet.remove() + }) +} export default { setPageCss, + clearPageCss, clearAllPageCSS } diff --git a/packages/runtime-renderer/src/renderer/page-function/index.ts b/packages/runtime-renderer/src/renderer/page-function/index.ts index 07d4adb65..11ced4704 100644 --- a/packages/runtime-renderer/src/renderer/page-function/index.ts +++ b/packages/runtime-renderer/src/renderer/page-function/index.ts @@ -11,4 +11,4 @@ */ export * from './css' -export * from './state' +export * from './lifecyle' diff --git a/packages/runtime-renderer/src/renderer/page-function/lifecyle.ts b/packages/runtime-renderer/src/renderer/page-function/lifecyle.ts new file mode 100644 index 000000000..fef82910b --- /dev/null +++ b/packages/runtime-renderer/src/renderer/page-function/lifecyle.ts @@ -0,0 +1,94 @@ +import { Notify } from '@opentiny/vue' +import { parseData } from '../data-function' +import type { JSFunction } from '../../types/index.ts' +import { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, + onDeactivated +} from 'vue' + +const executeUserLifecycle = (hookName: string, lifeCycleConfig: JSFunction | undefined, context: any) => { + if (!lifeCycleConfig || lifeCycleConfig.type !== 'JSFunction') { + return + } + + try { + const fn = parseData(lifeCycleConfig, {}, context) + if (typeof fn === 'function') { + fn.call(context, context) + } + } catch (error) { + Notify({ + type: 'warning', + title: `${hookName} 生命周期执行失败`, + message: (error as any)?.message || `${hookName} 生命周期函数执行报错,请检查语法` + }) + } +} +export function registerLifecycleHooks(lifeCycles: any, context: any) { + // 注册生命周期钩子 + if (lifeCycles?.setup) { + executeUserLifecycle('setup', lifeCycles?.setup, context) + } + + if (lifeCycles?.onBeforeMount) { + onBeforeMount(() => { + executeUserLifecycle('onBeforeMount', lifeCycles.onBeforeMount, context) + }) + } + + if (lifeCycles?.onMounted) { + onMounted(() => { + executeUserLifecycle('onMounted', lifeCycles.onMounted, context) + }) + } + + if (lifeCycles?.onBeforeUpdate) { + onBeforeUpdate(() => { + executeUserLifecycle('onBeforeUpdate', lifeCycles.onBeforeUpdate, context) + }) + } + + if (lifeCycles?.onUpdated) { + onUpdated(() => { + executeUserLifecycle('onUpdated', lifeCycles.onUpdated, context) + }) + } + + if (lifeCycles?.onBeforeUnmount) { + onBeforeUnmount(() => { + executeUserLifecycle('onBeforeUnmount', lifeCycles.onBeforeUnmount, context) + }) + } + + if (lifeCycles?.onUnmounted) { + onUnmounted(() => { + executeUserLifecycle('onUnmounted', lifeCycles.onUnmounted, context) + }) + } + + if (lifeCycles?.onErrorCaptured) { + onErrorCaptured((_error, _instance, _info) => { + executeUserLifecycle('onErrorCaptured', lifeCycles.onErrorCaptured, context) + return true + }) + } + + if (lifeCycles?.onActivated) { + onActivated(() => { + executeUserLifecycle('onActivated', lifeCycles.onActivated, context) + }) + } + + if (lifeCycles?.onDeactivated) { + onDeactivated(() => { + executeUserLifecycle('onDeactivated', lifeCycles.onDeactivated, context) + }) + } +} diff --git a/packages/runtime-renderer/src/renderer/page-function/state.ts b/packages/runtime-renderer/src/renderer/page-function/state.ts deleted file mode 100644 index 74d9736c2..000000000 --- a/packages/runtime-renderer/src/renderer/page-function/state.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { reactive } from 'vue' -import { getDeletedKeys } from '../../utils/data-utils' -import { isStateAccessor, parseData } from '../parser' -import { useAccessorMap } from './accessor' - -export function useState({ getContext }: { getContext: () => any }) { - const state = reactive>({}) - const { generateStateAccessors } = useAccessorMap(getContext()) - - const setState = (data: Record, clear?: boolean) => { - if (typeof data !== 'object' || data === null) { - return - } - - if (clear) { - Object.keys(state).forEach((key) => delete (state as any)[key]) - } - - // 智能删除处理:删除不再存在的状态键 - const deletedKeys = getDeletedKeys(state, data) - for (const key of deletedKeys) { - delete state[key] - } - - Object.assign(state, parseData(data, {}, getContext()) || {}) - - // 处理状态访问器 - Object.entries(data || {})?.forEach(([key, stateData]: [string, any]) => { - if (isStateAccessor(stateData)) { - const accessor = stateData.accessor - if (accessor?.getter?.value) { - generateStateAccessors('getter', accessor, key) - } - - if (accessor?.setter?.value) { - generateStateAccessors('setter', accessor, key) - } - } - }) - } - - return { - state, - setState - } -} diff --git a/packages/runtime-renderer/src/renderer/render.ts b/packages/runtime-renderer/src/renderer/render.ts index 60abcf705..7d7c6ba98 100644 --- a/packages/runtime-renderer/src/renderer/render.ts +++ b/packages/runtime-renderer/src/renderer/render.ts @@ -10,204 +10,33 @@ * */ -import { - h, - provide, - inject, - defineComponent, - onBeforeMount, - onMounted, - onBeforeUpdate, - onUpdated, - onBeforeUnmount, - onUnmounted, - onErrorCaptured, - onActivated, - onDeactivated -} from 'vue' -import { Notify } from '@opentiny/vue' -import { isHTMLTag, hyphenate } from '@vue/shared' -import TinyVue from '@opentiny/vue' -import { getBlockContext, getBlockCssScopeId } from './page-function/blockContext' -import { - CanvasRow, - CanvasCol, - CanvasRowColContainer, - CanvasFlexBox, - CanvasSection -} from '@opentiny/tiny-engine-builtin-component' -import { - CanvasIcon, - CanvasText, - CanvasSlot, - CanvasImg, - CanvasPlaceholder, - CanvasRouterLink, - CanvasRouterView, - CanvasCollection -} from './builtin' -import { parseData, parseCondition, parseLoopArgs } from './parser' - -const hyphenateRE = /\B([A-Z])/g -// 用于后续对Web component的扩展支持,目前暂未实际使用 -const customElements = {} - -const Mapper = { - Icon: CanvasIcon, - Text: CanvasText, - Slot: CanvasSlot, - slot: CanvasSlot, - Img: CanvasImg, - CanvasRow, - CanvasCol, - CanvasRowColContainer, - CanvasFlexBox, - CanvasSection, - CanvasPlaceholder, - RouterLink: CanvasRouterLink, - RouterView: CanvasRouterView, - Collection: CanvasCollection -} - -export const collectionMethodsMap = {} - -const getNative = (name) => { - return TinyVue?.[name] || window.TinyLowcodeComponent?.[name] -} - -const getBlock = (name) => { - return window.blocks?.[name] -} - -let pageScopeId = '' - -const setPageScopeId = (scopeId: string) => { - pageScopeId = scopeId -} - -const getPageScopeId = () => { - return pageScopeId -} - -export const getComponent = (name) => { - // 首先尝试从映射表、原生组件、自定义元素中获取 - const component = Mapper[name] || getNative(name) || customElements[name] - if (component) { - return component - } - - if (name === 'Template') { - return 'div' - } - - // 如果是 HTML 标签,直接返回 - if (isHTMLTag(name)) { - return name - } - - // 检查是否是区块组件 - const blockSchema = getBlock(name) - if (blockSchema) { - // 返回一个动态组件,用于渲染区块 - return defineComponent({ - name: `${name}`, - setup() { - // 区块的真实内容在 window.blocks 中,而不是页面的 schema 中 - // 页面的 schema 只是区块的引用,children 为空 - const blockContent = blockSchema.schema - - const context = getBlockContext(blockContent) - - return { - context - } - }, - render() { - // 递归渲染区块的 children - const blockContent = blockSchema.schema - const context = this.context - const cssScopeId = getBlockCssScopeId(blockSchema.schema.fileName) - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const children = renderGroup(blockContent.children || [], { cssScopeId }, context, renderComponent) - const pageScopeId = getPageScopeId() - - return h( - 'div', - { - [pageScopeId]: '' - }, - h('div', { [cssScopeId]: '', ...blockContent.props }, children) - ) - } +import { defineComponent, h, inject, provide, Suspense } from 'vue' +import { registerLifecycleHooks } from './page-function/index.ts' +import { NODE_TAG, NODE_LOOP, NODE_UID } from './app-function/constant.ts' +import { parseCondition, parseData, parseLoopArgs } from './data-function/index.ts' +import { blockSlotDataMap, getComponent, Mapper } from './material-function/index.ts' +import Loading from '../components/Loading.vue' +import BlockLoading from '../components/BlockLoading.vue' +import type { Node } from '../types/index.ts' + +export const renderDefault = (children: Node[], scope: Record, parent: Node) => + children.map?.((child) => + // eslint-disable-next-line @typescript-eslint/no-use-before-define + h(renderer, { + schema: child, + scope, + parent }) - } - - return CanvasPlaceholder -} + ) -const configure = {} - -export const setConfigure = (configureData) => { - Object.assign(configure, configureData) -} - -const _getPlainProps = (object = {}) => { - const { slot, ...rest } = object - const props = {} - - if (slot) { - rest.slot = slot.name || slot - } - - Object.entries(rest).forEach(([key, value]) => { - let renderKey = key - - // html 标签属性会忽略大小写,所以传递包含大写的 props 需要转换为 kebab 形式的 props - if (!/on[A-Z]/.test(renderKey) && hyphenateRE.test(renderKey)) { - renderKey = hyphenate(renderKey) - } - - if (['boolean', 'string', 'number'].includes(typeof value)) { - props[renderKey] = value - } else { - // 如果传给webcomponent标签的是对象或者数组需要使用.prop修饰符,转化成h函数就是如下写法 - props[`.${renderKey}`] = value - } - }) - return props -} - -const generateCollection = (schema) => { - if (schema.componentName === 'Collection' && schema.props?.dataSource && schema.children) { - schema.children.forEach((item) => { - const fetchData = item.props?.fetchData - const methodMatch = fetchData?.value?.match(/this\.(.+?)}/) - if (fetchData && methodMatch?.[1]) { - const methodName = methodMatch[1].trim() - // 缓存表格fetchData对应的数据源信息 - collectionMethodsMap[methodName] = schema.props.dataSource - } - }) - } -} - -export const renderDefault = ( - children: any[], - scope: Record, - parent: any, - renderComponent: (schema: any, scope: Record, parent: any) => any -) => children.map?.((child) => renderComponent(child, scope, parent)) -const generateSlotGroup = (children, isCustomElm, schema) => { - const slotGroup = {} +const generateSlotGroup = (children: Node[], schema: Node) => { + const slotGroup: Record = {} children.forEach((child) => { const { componentName, children, params = [], props } = child const slot = child.slot || props?.slot?.name || props?.slot || 'default' - const isNotEmptyTemplate = componentName === 'Template' && children.length + const isNotEmptyTemplate = componentName === 'Template' && children?.length - if (isCustomElm) { - child.props.slot = 'slot' // CE下需要给子节点加上slot标识 - } slotGroup[slot] = slotGroup[slot] || { value: [], params, @@ -220,50 +49,53 @@ const generateSlotGroup = (children, isCustomElm, schema) => { return slotGroup } -const renderSlot = (children, scope, schema, isCustomElm, context, renderComponent) => { - if (children.some((a) => a.componentName === 'Template')) { - const slotGroup = generateSlotGroup(children, isCustomElm, schema) - const slots = {} - - Object.keys(slotGroup).forEach((slotName) => { - const currentSlot = slotGroup[slotName] - - slots[slotName] = ($scope) => renderDefault(currentSlot.value, { ...scope, ...$scope }, context, renderComponent) - }) +const directChildrenHasTemplate = (children: Node[]) => children.some((child) => child.componentName === 'Template') - return slots - } +const renderSlot = ( + children: Node[], + scope: Record, + schema: Node, + pageContext: Record, + renderComponent: (schema: Node, scope: Record, pageContext: Record, parent: Node) => any +) => { + const slotGroup = generateSlotGroup(children, schema) + const slots: Record = {} + Object.keys(slotGroup).forEach((slotName) => { + const currentSlot = slotGroup[slotName] + slots[slotName] = ($scope: Record) => + currentSlot.value.map((slotItem: Node) => + renderComponent(slotItem, { ...scope, ...$scope }, pageContext, currentSlot.parent) + ) + }) - return { default: () => renderDefault(children, scope, context, renderComponent) } + return slots } -const _checkGroup = (componentName) => configure[componentName]?.nestingRule?.childWhitelist?.length - -const directChildrenHasTemplate = (children) => children.some((child) => child.componentName === 'Template') - -const getBindProps = (schema, scope, context) => { - const { componentName, componentType } = schema +const getBindProps = ( + schema: Node, + scope: Record, + context: Record, + pageContext: Record +) => { + const { id, componentName, componentType } = schema if (componentName === 'CanvasPlaceholder') { return {} } - + const { active, getCssScopeId } = pageContext || {} + const cssScopeId = getCssScopeId?.() const bindProps = { - ...parseData(schema.props, scope, context) + ...parseData(schema.props, scope, context), + ...(cssScopeId ? { [cssScopeId]: '' } : {}), + ...{ [NODE_UID]: id }, + [NODE_TAG]: componentName } - const cssScopeId = scope?.cssScopeId || context?.cssScopeId || context?.getCssScopeId?.() - if (cssScopeId && componentType !== 'Block') { - bindProps[cssScopeId] = '' + if (scope) { + bindProps[NODE_LOOP] = scope.index === undefined ? scope.idx : scope.index } - if (Mapper[componentName]) { - bindProps.schema = schema - } - - // 如果是区块组件,传递完整的 schema - const blockSchema = getBlock(componentName) - if (blockSchema) { + if (Mapper[componentName as keyof typeof Mapper]) { bindProps.schema = schema } @@ -271,10 +103,25 @@ const getBindProps = (schema, scope, context) => { bindProps.class = bindProps.className delete bindProps.className + // 使画布中元素可拖拽 + if (active && !['PageStart', 'PageSection'].includes(componentType || '')) { + bindProps.draggable = true + } + return bindProps } -const getLoopScope = ({ scope, index, item, loopArgs }) => { +const getLoopScope = ({ + scope, + index, + item, + loopArgs +}: { + scope: Record + index: number + item: any + loopArgs: any +}) => { return { ...scope, ...(parseLoopArgs({ @@ -285,132 +132,88 @@ const getLoopScope = ({ scope, index, item, loopArgs }) => { } } -const injectPlaceHolder = (componentName, children) => { - const isEmptyArr = Array.isArray(children) && !children.length +const getChildren = ( + schema: Node, + mergeScope: Record, + pageContext: Record, + parent: Node, + renderComponent: (schema: Node, scope: Record, pageContext: Record, parent: Node) => any +) => { + const { children = [] } = schema + const renderChildren = children as Node[] - if (configure[componentName]?.isContainer && (!children || isEmptyArr)) { - return [ - { - componentName: 'CanvasPlaceholder' - } - ] - } - - return children -} - -const renderGroup = (children, scope, context, renderComponent) => { - return children.map?.((schema) => { - const { componentName, children, loop, loopArgs, condition } = schema - const loopList = parseData(loop, scope, context) - - const renderElement = (item, index) => { - const mergeScope = getLoopScope({ - scope, - index, - item, - loopArgs - }) - - if (!parseCondition(condition, mergeScope, context)) { - return null - } - - const renderChildren = injectPlaceHolder(componentName, children) - - const element = h( - getComponent(componentName), - getBindProps(schema, mergeScope, context), - Array.isArray(renderChildren) - ? renderSlot(renderChildren, mergeScope, schema, customElements[componentName], context, renderComponent) - : parseData(renderChildren, mergeScope, context) - ) - - return element + if (Array.isArray(renderChildren)) { + // children 空的场景,不能返回空数组,因为有部分组件会误以为使用了自定义插槽,从而无法渲染默认插槽内容,比如 TinyTree 组件 + if (!renderChildren.length) { + return null } - return loopList?.length ? loopList.map(renderElement) : renderElement(undefined, 0) - }) -} -const getChildren = (schema, mergeScope, context, renderComponent) => { - const { componentName, children } = schema - const renderChildren = injectPlaceHolder(componentName, children) - - if (!Array.isArray(renderChildren)) { - return parseData(renderChildren, mergeScope, context) - } - - if (!renderChildren.length) { - return null - } - - const isCustomElm = customElements[componentName] + if (directChildrenHasTemplate(renderChildren)) { + return renderSlot(renderChildren, mergeScope, schema, pageContext, renderComponent) + } - if (directChildrenHasTemplate(renderChildren)) { - return renderSlot(renderChildren, mergeScope, schema, isCustomElm, context, renderComponent) + // 这里 children 需要返回一个默认插槽的函数,避免 vue 告警: + // Non-function value encountered for default slot. Prefer function slots for better performance. + return { + default: () => renderChildren.map((child) => renderComponent(child, mergeScope, pageContext, parent)) + } } - return renderGroup(renderChildren, mergeScope, context, renderComponent) + return parseData(renderChildren, mergeScope, pageContext) } -function renderComponent(schema, scope, parent) { - const { componentName, loop, loopArgs, condition } = schema - - // 处理数据源和表格fetchData的映射关系 - generateCollection(schema) +const renderComponent = (schema: Node, scope: Record, pageContext: Record, parent: Node) => { + const { componentName, loop, loopArgs, condition } = schema as any if (!componentName) { - return parseData(schema, scope, parent) + return parseData(schema, scope, pageContext) } - const component = getComponent(componentName) + const loopList = loop ? parseData(loop, scope, pageContext) : [] - const loopList = parseData(loop, scope, parent) - - const renderElement = (item, index) => { - const mergeScope = item + const renderElement = (item?: Node, index: number = 0) => { + let mergeScope = item ? getLoopScope({ - item, + scope, index, - loopArgs, - scope + item, + loopArgs }) : scope - if (!parseCondition(condition, mergeScope, parent)) { + if (!parseCondition(condition, mergeScope, pageContext)) { return null } + // 如果是区块,并且使用了区块的作用域插槽,则需要将作用域插槽的数据传递下去 + if (parent?.componentType === 'Block' && componentName === 'Template' && schema.props?.slot?.params?.length) { + const slotName = schema.props.slot?.name || schema.props.slot + const blockName = parent.componentName + const slotData = blockSlotDataMap[blockName]?.[slotName] || {} + mergeScope = mergeScope ? { ...mergeScope, ...slotData } : slotData + } const Ele = h( - component, - getBindProps(schema, mergeScope, parent), - getChildren(schema, mergeScope, parent, renderComponent) + getComponent(componentName), + getBindProps(schema, mergeScope, pageContext, pageContext), + getChildren(schema, mergeScope, pageContext, parent, renderComponent) ) + // 区块加上 suspense 渲染,就可以在网络延时的时候显示加载中的字样或者动画,优化体验 + if (schema.componentType === 'Block') { + return h( + Suspense, + {}, + { + default: () => Ele, + fallback: () => h(BlockLoading, { name: componentName }) + } + ) + } + return Ele } - return loopList?.length ? loopList.map(renderElement) : renderElement(undefined, 0) -} - -// 执行用户定义的生命周期函数 -const executeUserLifecycle = (hookName: string, lifeCycleConfig: JSFunction | undefined, context: any) => { - if (!lifeCycleConfig || lifeCycleConfig.type !== 'JSFunction') { - return - } - - try { - const fn = parseData(lifeCycleConfig, {}, context) - if (typeof fn === 'function') { - fn.call(context, context) - } - } catch (error) { - Notify({ - type: 'warning', - title: `${hookName} 生命周期执行失败`, - message: (error as any)?.message || `${hookName} 生命周期函数执行报错,请检查语法` - }) - } + return loopList?.length ? loopList.map(renderElement) : renderElement() } export const renderer = defineComponent({ @@ -422,90 +225,23 @@ export const renderer = defineComponent({ }, setup(props) { provide('schema', props.schema) - - const context = inject('pageContext') + const pageContext = inject('pageContext') || {} const lifeCycles = props.parent?.lifeCycles - const pageScopeId = context?.cssScopeId || context?.getCssScopeId?.() - setPageScopeId(pageScopeId) - - // 注入生命周期钩子 - if (lifeCycles?.setup) { - executeUserLifecycle('setup', lifeCycles?.setup, context) - } - - if (lifeCycles?.onBeforeMount) { - onBeforeMount(() => { - executeUserLifecycle('onBeforeMount', lifeCycles.onBeforeMount, context) - }) - } - - if (lifeCycles?.onMounted) { - onMounted(() => { - executeUserLifecycle('onMounted', lifeCycles.onMounted, context) - }) - } - - if (lifeCycles?.onBeforeUpdate) { - onBeforeUpdate(() => { - executeUserLifecycle('onBeforeUpdate', lifeCycles.onBeforeUpdate, context) - }) - } - - if (lifeCycles?.onUpdated) { - onUpdated(() => { - executeUserLifecycle('onUpdated', lifeCycles.onUpdated, context) - }) - } - - if (lifeCycles?.onBeforeUnmount) { - onBeforeUnmount(() => { - executeUserLifecycle('onBeforeUnmount', lifeCycles.onBeforeUnmount, context) - }) - } - - if (lifeCycles?.onUnmounted) { - onUnmounted(() => { - executeUserLifecycle('onUnmounted', lifeCycles.onUnmounted, context) - }) - } - - if (lifeCycles?.onErrorCaptured) { - onErrorCaptured((error, instance, info) => { - try { - const fn = parseData(lifeCycles.onErrorCaptured, {}, context) - if (typeof fn === 'function') { - const result = fn.call(context, error, instance, info) - return result === false - } - } catch (userError) { - Notify({ - type: 'warning', - title: 'onErrorCaptured 生命周期执行失败', - message: (userError as any)?.message || 'onErrorCaptured 生命周期函数执行报错,请检查语法' - }) - } - return true - }) - } - - if (lifeCycles?.onActivated) { - onActivated(() => { - executeUserLifecycle('onActivated', lifeCycles.onActivated, context) - }) - } - - if (lifeCycles?.onDeactivated) { - onDeactivated(() => { - executeUserLifecycle('onDeactivated', lifeCycles.onDeactivated, context) - }) - } + registerLifecycleHooks(lifeCycles, pageContext) + return { pageContext } }, render() { - const context = inject('pageContext') - const { scope, schema } = this - - return renderComponent(schema, scope, context, renderComponent) + const { scope = {}, schema, parent, pageContext } = this + return renderComponent(schema as Node, scope, pageContext, parent as Node) } }) -export default renderer +export function defaultRenderer(schema: Node) { + const PageStartSchema = { + componentName: 'div', + componentType: 'PageStart', + props: { 'data-id': 'page-root-container', ...(schema.props || {}) }, + children: schema.children + } + return schema.children?.length ? h(renderer, { schema: PageStartSchema, parent: schema }) : [h(Loading)] +} diff --git a/packages/runtime-renderer/src/renderer/useContext.ts b/packages/runtime-renderer/src/renderer/useContext.ts deleted file mode 100644 index 1db4ece66..000000000 --- a/packages/runtime-renderer/src/renderer/useContext.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2023 - present TinyEngine Authors. - * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. - * - * Use of this source code is governed by an MIT-style license. - * - * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, - * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR - * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. - * - */ - -import { shallowReactive, type ShallowReactive } from 'vue' - -interface Context { - [key: string]: any -} - -interface UseContextReturn { - context: ShallowReactive - setContext: (ctx: Context, clear?: boolean) => void - getContext: () => ShallowReactive -} - -export default (): UseContextReturn => { - const context = shallowReactive({}) - - const setContext = (ctx: Context, clear?: boolean) => { - if (clear) { - Object.keys(context).forEach((key) => delete context[key]) - } - Object.assign(context, ctx) - } - - const getContext = () => context - - return { - context, - setContext, - getContext - } -} diff --git a/packages/runtime-renderer/src/types/index.ts b/packages/runtime-renderer/src/types/index.ts new file mode 100644 index 000000000..1e21747f0 --- /dev/null +++ b/packages/runtime-renderer/src/types/index.ts @@ -0,0 +1,31 @@ +export * from './schema' +export * from './config' +export interface Node { + id: string + componentName: string + props: Record & { columns?: { slots?: Record }[] } + children?: Node[] + componentType?: 'Block' | 'PageStart' | 'PageSection' + slot?: string | Record + params?: string[] + loop?: Record + loopArgs?: string[] + condition?: boolean | Record + lifeCycles?: Record +} + +export type RootNode = Omit & { + id?: string + css?: string + fileName?: string + methods?: Record + state?: Record + lifeCycles?: Record + dataSource?: any + bridge?: any + inputs?: any[] + outputs?: any[] + schema?: any +} + +export type IFuntion = (...args: any[]) => any diff --git a/packages/runtime-renderer/src/types/schema.ts b/packages/runtime-renderer/src/types/schema.ts index 2a959ee21..6bdd03724 100644 --- a/packages/runtime-renderer/src/types/schema.ts +++ b/packages/runtime-renderer/src/types/schema.ts @@ -1,5 +1,6 @@ // 应用级Schema类型定义 export interface IAppSchema { + id: string pages: any[] bridge: any[] componentsMap: ComponentMap[] @@ -13,6 +14,7 @@ export interface IAppSchema { constants: string i18n: I18nConfig version: string + blocks: Record } // 组件映射表 @@ -280,6 +282,16 @@ export interface BlockItem { current_version?: any } +export interface IBlockItem { + schema: BlockContent + meta: { + id: number + label: string + framework: string + version: string + } +} + // 区块内容 export interface BlockContent { componentName: string diff --git a/packages/runtime-renderer/types.d.ts b/packages/runtime-renderer/types.d.ts index 0aea3f333..9176ea946 100644 --- a/packages/runtime-renderer/types.d.ts +++ b/packages/runtime-renderer/types.d.ts @@ -5,3 +5,9 @@ export declare global { blocks: Record } } +export declare module '@opentiny/tiny-engine-dsl-vue' { + // 先声明原有导出(避免覆盖库的默认类型) + export type * from '@opentiny/tiny-engine-dsl-vue' + // 补充 genSFCWithDefaultPlugin 的声明(根据实际参数/返回值调整类型) + export function genSFCWithDefaultPlugin(schema?: any, componentsMap?: any, config = {}, nextPage?: any): any +} diff --git a/packages/runtime-renderer/vite.config.ts b/packages/runtime-renderer/vite.config.ts index 78d3ed9ac..1ac209c22 100644 --- a/packages/runtime-renderer/vite.config.ts +++ b/packages/runtime-renderer/vite.config.ts @@ -5,20 +5,23 @@ import { resolve } from 'path' export default defineConfig({ plugins: [vue()], build: { + assetsDir: '', outDir: 'dist', lib: { - entry: resolve(__dirname, 'index.ts'), - name: 'TinyEngineRuntimeRenderer', - fileName: 'index' + entry: { + index: resolve(__dirname, 'index.ts') + }, + formats: ['es'], + name: 'runtime-renderer', + fileName: (_, entryName) => `${entryName}.js` }, rollupOptions: { - external: ['vue', '@vueuse/core', 'vue-i18n', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/], output: { - globals: { - vue: 'Vue', - '@opentiny/vue': 'TinyVue' + banner: (chunk) => { + return ['index'].includes(chunk.name) && chunk.isEntry ? `import "./style.css"` : '' } - } + }, + external: ['vue', '@vueuse/core', 'vue-i18n', /@opentiny\/tiny-engine.*/, /@opentiny\/vue.*/] } } }) From 021f9562854840c1a9b083ba6764c7893fddc830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=95=B8?= Date: Wed, 17 Dec 2025 15:11:57 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4runtime=20?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E7=BC=96=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vite-config/src/canvas-dev-external.js | 37 +++---- .../build/vite-config/src/default-config.js | 7 +- .../build/vite-config/src/runtime-external.js | 98 +++++++++++++++---- .../src/composables/service.ts | 19 ++-- .../src/composables/useAppSchema.ts | 16 ++- .../src/renderer/app-function/importMap.ts | 9 +- 6 files changed, 124 insertions(+), 62 deletions(-) diff --git a/packages/build/vite-config/src/canvas-dev-external.js b/packages/build/vite-config/src/canvas-dev-external.js index 464f11d7f..86db5844f 100644 --- a/packages/build/vite-config/src/canvas-dev-external.js +++ b/packages/build/vite-config/src/canvas-dev-external.js @@ -1,37 +1,38 @@ import vitePluginExternalize from 'vite-plugin-externalize-dependencies' import { genImportMapPlugin } from './vite-plugins/genImportMapOnly.js' -export function canvasDevExternal(override = {}) { - const prefix = '/node_modules/@opentiny/tiny-engine' - // 以下内容由于区块WebComponent加载需要补充 - const blockRequire = { - externals: [/^@opentiny\/vue$/, /^@opentiny\/vue-icon$/], +export const prefix = '/node_modules/@opentiny/tiny-engine' +export const dependencies = { + base: { imports: { - '@opentiny/vue': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-pc.mjs`, - '@opentiny/vue-icon': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-icon.mjs` + vue: `${prefix}/node_modules/vue/dist/vue.runtime.esm-browser.js`, + 'vue-i18n': `${prefix}/node_modules/vue-i18n/dist/vue-i18n.esm-browser.js` }, - importStyles: [`${prefix}/node_modules/@opentiny/vue-theme/index.css`] - } - // 以下内容由于物料协议不支持声明子依赖而@opentiny/vue需要依赖所以需要补充 - const tinyVueRequire = { + externals: [/^vue$/, /^vue-i18n$/] + }, + ui: { imports: { + '@opentiny/vue': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-pc.mjs`, + '@opentiny/vue-icon': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-icon.mjs`, '@opentiny/vue-common': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-common.mjs`, '@opentiny/vue-locale': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-locale.mjs` - } + }, + externals: [/^@opentiny\/vue$/, /^@opentiny\/vue-icon$/, /^@opentiny\/vue-common$/, /^@opentiny\/vue-locale$/], + importStyles: [`${prefix}/node_modules/@opentiny/vue-theme/index.css`] } +} +export function canvasDevExternal(override = {}) { return [ - vitePluginExternalize({ externals: [/^vue$/, /^vue-i18n$/, ...blockRequire.externals] }), + vitePluginExternalize({ externals: [...dependencies.base.externals, ...dependencies.ui.externals] }), genImportMapPlugin( { imports: { - vue: `${prefix}/node_modules/vue/dist/vue.runtime.esm-browser.js`, - 'vue-i18n': `${prefix}/node_modules/vue-i18n/dist/vue-i18n.esm-browser.js`, - ...blockRequire.imports, - ...tinyVueRequire.imports, + ...dependencies.base.imports, + ...dependencies.ui.imports, ...override } }, - [...blockRequire.importStyles] + [...dependencies.ui.importStyles] ) ] } diff --git a/packages/build/vite-config/src/default-config.js b/packages/build/vite-config/src/default-config.js index 0ace8cac2..010bf4e14 100644 --- a/packages/build/vite-config/src/default-config.js +++ b/packages/build/vite-config/src/default-config.js @@ -12,8 +12,9 @@ import generateComment from '@opentiny/tiny-engine-vite-plugin-meta-comments' import { getBaseUrlFromCli, copyBundleDeps, importMapLocalPlugin } from './localCdnFile/index.js' import { devAliasPlugin } from './vite-plugins/devAliasPlugin.js' import { htmlUpgradeHttpsPlugin } from './vite-plugins/upgradeHttpsPlugin.js' -import { canvasDevExternal } from './canvas-dev-external.js' import { treeShakingPlugin } from './vite-plugins/treeShakingPlugin.js' +import { canvasDevExternal } from './canvas-dev-external.js' +import { runtimeExternal } from './runtime-external.js' const monacoEditorPlugin = monacoEditorPluginCjs.default const nodeGlobalsPolyfillPlugin = nodeGlobalsPolyfillPluginCjs.default @@ -188,5 +189,9 @@ export function useTinyEngineBaseConfig(engineConfig) { config.plugins.push(canvasDevExternal()) } + if (engineConfig.useSourceAlias && command !== 'serve') { + config.plugins.push(runtimeExternal()) + } + return config } diff --git a/packages/build/vite-config/src/runtime-external.js b/packages/build/vite-config/src/runtime-external.js index 8c32d007d..d051137f3 100644 --- a/packages/build/vite-config/src/runtime-external.js +++ b/packages/build/vite-config/src/runtime-external.js @@ -1,20 +1,84 @@ -export const prefix = '/node_modules/@opentiny/tiny-engine' -export const dependencies = { - base: { - imports: { - vue: `${prefix}/node_modules/vue/dist/vue.runtime.esm-browser.js`, - 'vue-i18n': `${prefix}/node_modules/vue-i18n/dist/vue-i18n.esm-browser.js` - }, - externals: [/^vue$/, /^vue-i18n$/] - }, - ui: { - imports: { - '@opentiny/vue': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-pc.mjs`, - '@opentiny/vue-icon': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-icon.mjs`, - '@opentiny/vue-common': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-common.mjs`, - '@opentiny/vue-locale': `${prefix}/node_modules/@opentiny/vue-runtime/dist3/tiny-vue-locale.mjs` +import { dependencies } from './canvas-dev-external.js' + +/** + * 嵌入