From d7c8a3a9e1fc5c8e596a17cdadb7079d20e52297 Mon Sep 17 00:00:00 2001
From: zhengyiming <540361168@qq.com>
Date: 星期四, 21 八月 2025 15:12:02 +0800
Subject: [PATCH] fix: 签约

---
 src/fabric-editor/viewcomponents/right/index.vue                    |  194 
 src/fabric-editor/assets/icon/layer/textbox.svg                     |    1 
 src/fabric-editor/assets/icon/layer/upTop.svg                       |    1 
 src/fabric-editor/assets/icon/attribute/textAlignRight.svg          |    1 
 src/fabric-editor/components/login.vue                              |  244 
 src/fabric-editor/components/lang.vue                               |   52 
 src/fabric-editor/assets/icon/proIcon.png                           |    0 
 src/fabric-editor/components/systemTemplateDataParamSetting.vue     |  187 
 src/fabric-editor/components/color-picker/comps/AngleHandle.vue     |  162 
 src/utils/common/pdf.ts                                             |   77 
 src/fabric-editor/assets/icon/top.svg                               |    1 
 auto-imports.d.ts                                                   |   11 
 src/fabric-editor/assets/filters/Sepia.png                          |    0 
 src/fabric-editor/components/color-picker/comps/svg.vue             |   28 
 src/fabric-editor/assets/fonts/font.css                             |    9 
 src/fabric-editor/language/pt.json                                  |  128 
 src/fabric-editor/assets/icon/attribute/linethrough.svg             |    5 
 src/fabric-editor/components/color-picker/index.vue                 |  791 ++
 src/fabric-editor/components/attributeTextContent.vue               |  147 
 src/fabric-editor/assets/icon/tools/draw4.svg                       |   15 
 src/fabric-editor/assets/icon/attribute/textAlignCenter.svg         |    1 
 src/fabric-editor/components/bgBar.vue                              |  117 
 src/fabric-editor/assets/icon/tools/triangle.svg                    |   14 
 src/fabric-editor/components/group.vue                              |   43 
 src/fabric-editor/components/common/typeList.vue                    |  129 
 src/utils/common/index.ts                                           |    1 
 src/fabric-editor/assets/icon/align/right.svg                       |    4 
 src/fabric-editor/components/logo.vue                               |   64 
 src/fabric-editor/assets/icon/layer/rect.svg                        |    1 
 src/fabric-editor/components/importFile.vue                         |  136 
 src/fabric-editor/components/workspaceMask.vue                      |   37 
 src/fabric-editor/assets/icon/align/centerX.svg                     |   14 
 src/fabric-editor/assets/icon/zoom/big.svg                          |   20 
 src/fabric-editor/components/common/modalSzie.vue                   |  100 
 src/fabric-editor/hooks/pageList.js                                 |  210 
 src/fabric-editor/styles/variable.less                              |    0 
 src/fabric-editor/assets/filters/Brownie.png                        |    0 
 src/constants/apiEnum.ts                                            |   12 
 src/fabric-editor/assets/icon/sx.svg                                |    1 
 src/fabric-editor/components/previewImageList.vue                   |   72 
 src/fabric-editor/assets/icon/align/averageX.svg                    |    6 
 src/fabric-editor/assets/icon/attribute/fontWeight.svg              |    5 
 src/fabric-editor/components/align.vue                              |  110 
 src/fabric-editor/assets/filters/Polaroid.png                       |    0 
 src/fabric-editor/assets/filters/Vintage.png                        |    0 
 src/fabric-editor/config/constants/filter.ts                        |  192 
 src/fabric-editor/assets/icon/align/top.svg                         |    6 
 src/fabric-editor/components/flip.vue                               |   58 
 src/fabric-editor/config/constants/app.ts                           |    1 
 src/services/api/index.ts                                           |    8 
 src/fabric-editor/components/attributeFont.vue                      |  426 +
 src/fabric-editor/hooks/useCalculate.js                             |   26 
 src/fabric-editor/components/previewCurrent.vue                     |   29 
 src/fabric-editor/hooks/useAdmin.js                                 |   92 
 src/fabric-editor/plugin/fabric-history.js                          |  224 
 src/fabric-editor/components/attributeTextFloat.vue                 |  172 
 src/fabric-editor/components/myMaterial/components/fileType.vue     |  151 
 src/fabric-editor/language/index.ts                                 |   48 
 src/fabric-editor/viewcomponents/left/index.vue                     |  123 
 src/fabric-editor/components/attributeQrCode.vue                    |  335 +
 src/fabric-editor/hooks/select.ts                                   |   96 
 src/fabric-editor/styles/contextMenu.scss                           |   56 
 src/fabric-editor/components/dragMode.vue                           |   48 
 src/fabric-editor/components/cropperDialog.vue                      |  290 +
 src/fabric-editor/assets/icon/tools/draw3.svg                       |   15 
 src/fabric-editor/components/inputNumber/inputNumber.vue            |  555 ++
 src/fabric-editor/assets/icon/layer/up.svg                          |    1 
 src/fabric-editor/components/common/searchType.vue                  |  121 
 index.html                                                          |    1 
 src/fabric-editor/components/centerAlign.vue                        |   63 
 src/fabric-editor/components/color-picker/index.css                 |  107 
 src/fabric-editor/assets/icon/centerAlign/center.svg                |   18 
 src/fabric-editor/hooks/useMaterial.js                              |  175 
 src/fabric-editor/components/setSize.vue                            |   82 
 src/views/ProtocolManage/EditTemplate.vue                           |   59 
 src/fabric-editor/components/clipImage.vue                          |   90 
 src/fabric-editor/components/importTmpl.vue                         |  180 
 src/fabric-editor/components/color-picker/comps/TabPanel.vue        |   39 
 src/fabric-editor/components/layer.vue                              |  258 
 src/fabric-editor/components/attributeId.vue                        |  112 
 src/fabric-editor/assets/filters/BlackWhite.png                     |    0 
 src/fabric-editor/components/myTemplName.vue                        |  137 
 src/services/api/electronSign.ts                                    |   35 
 src/fabric-editor/components/edit.vue                               |   21 
 src/fabric-editor/assets/icon/layer/default.svg                     |    1 
 src/fabric-editor/components/zoom.vue                               |   55 
 src/fabric-editor/components/material.vue                           |  125 
 src/fabric-editor/assets/icon/layer/group.svg                       |    1 
 src/fabric-editor/components/color-picker/utils/moveable.ts         |   73 
 src/fabric-editor/hooks/usePageList.js                              |  138 
 src/fabric-editor/assets/icon/align/centerY.svg                     |    6 
 src/fabric-editor/components/history.vue                            |   58 
 src/fabric-editor/assets/icon/group/group.svg                       |   13 
 src/fabric-editor/assets/icon/barcode/left.svg                      |    1 
 src/fabric-editor/components/attributeBarcode.vue                   |  315 +
 src/views/DictionaryManage/components/AddOrEditDictionaryDialog.vue |   11 
 src/fabric-editor/styles/resizePlugin.scss                          |   33 
 src/fabric-editor/assets/icon/group/unGroup.svg                     |   13 
 src/fabric-editor/components/myMaterial/components/file.vue         |  196 
 src/constants/dic.ts                                                |    2 
 src/fabric-editor/utils/math.ts                                     |   25 
 src/views/DictionaryManage/DataDictionary.vue                       |    3 
 src/fabric-editor/assets/icon/zoom/small.svg                        |   20 
 src/fabric-editor/components/inputNumber/index.ts                   |    3 
 src/fabric-editor/components/color-picker/utils/helper.ts           |   58 
 src/fabric-editor/components/attributeColor.vue                     |  161 
 src/fabric-editor/assets/icon/bottom.svg                            |    1 
 src/fabric-editor/assets/icon/centerAlign/centerY.svg               |   12 
 src/fabric-editor/assets/icon/fileType.png                          |    0 
 src/fabric-editor/language/zh.json                                  |  263 
 package.json                                                        |    4 
 src/fabric-editor/customObject/index.ts                             |   55 
 src/fabric-editor/assets/icon/tools/qrCode.svg                      |   23 
 src/fabric-editor/hooks/context.ts                                  |  219 
 src/fabric-editor/viewcomponents/top/index.vue                      |   89 
 src/fabric-editor/components/waterMark.vue                          |  181 
 src/fabric-editor/styles/resetViewUi.less                           |   53 
 src/fabric-editor/types.d.ts                                        |   19 
 src/fabric-editor/components/svgIcon/index.vue                      |   63 
 src/fabric-editor/assets/fonts/cn/汉体.ttf                            |    0 
 src/fabric-editor/assets/icon/align/left.svg                        |   10 
 src/fabric-editor/assets/icon/tools/draw2.svg                       |   15 
 src/fabric-editor/components/attributeTemplateParam.vue             |  134 
 src/fabric-editor/assets/icon/attribute/textAlignJustitfy.svg       |    1 
 src/fabric-editor/components/replaceImg.vue                         |   71 
 src/fabric-editor/components/attributePostion.vue                   |  172 
 src/fabric-editor/assets/icon/tools/rect.svg                        |   14 
 src/constants/electronSign.ts                                       |   19 
 src/fabric-editor/assets/fonts/cn/华康金刚黑.ttf                         |    0 
 src/fabric-editor/assets/icon/sy.svg                                |    1 
 src/fabric-editor/utils/index.ts                                    |   32 
 src/fabric-editor/components/svgIcon/index.js                       |   15 
 src/fabric-editor/components/clone.vue                              |   15 
 src/fabric-editor/components/attributeShadow.vue                    |  148 
 src/fabric-editor/hooks/useSelectListen.ts                          |   68 
 src/fabric-editor/components/admin.vue                              |  100 
 src/fabric-editor/components/hide.vue                               |   43 
 src/fabric-editor/assets/filters/technicolor.png                    |    0 
 src/fabric-editor/assets/icon/attribute/fontStyle.svg               |    5 
 src/fabric-editor/components/common/pageList.vue                    |  140 
 src/fabric-editor/styles/index.less                                 |   32 
 src/views/ProtocolManage/TemplateKeyEdit.vue                        |   18 
 src/fabric-editor/components/lock.vue                               |   75 
 src/fabric-editor/assets/icon/align/bottom.svg                      |    6 
 src/fabric-editor/assets/icon/layer/downTop.svg                     |    1 
 src/fabric-editor/assets/fonts/font.js                              |   22 
 src/fabric-editor/index.vue                                         |  266 
 src/fabric-editor/assets/icon/attribute/textAlignLeft.svg           |    1 
 src/fabric-editor/assets/icon/centerAlign/centerX.svg               |   12 
 src/fabric-editor/assets/icon/centerx.svg                           |    1 
 src/fabric-editor/assets/icon/layer/triangle.svg                    |    1 
 src/fabric-editor/components/imgStroke.vue                          |  170 
 .eslintrc-auto-import.json                                          |    4 
 src/fabric-editor/assets/icon/layer/circle.svg                      |    1 
 src/fabric-editor/assets/icon/layer/down.svg                        |    1 
 src/fabric-editor/assets/icon/layer/iText.svg                       |    1 
 src/fabric-editor/components/color-picker/utils/color.ts            |  130 
 src/fabric-editor/language/en.json                                  |  195 
 src/fabric-editor/hooks/useFileType.js                              |   40 
 src/fabric-editor/components/del.vue                                |   33 
 pnpm-lock.yaml                                                      | 1363 ++++
 src/fabric-editor/components/colorSelector.vue                      |  278 +
 src/fabric-editor/assets/icon/flip/y.svg                            |   21 
 src/fabric-editor/components/fontStyle.vue                          |  136 
 src/fabric-editor/components/color-picker/utils/tool.ts             |    6 
 src/fabric-editor/assets/icon/barcode/right.svg                     |    1 
 src/router/index.ts                                                 |   10 
 src/fabric-editor/components/importJSON.vue                         |   81 
 src/services/api/ocrUtils.ts                                        |   43 
 src/fabric-editor/utils/local.ts                                    |   36 
 src/fabric-editor/components/save.vue                               |  112 
 src/fabric-editor/plugin/MyHistoryPlugin.ts                         |   10 
 src/fabric-editor/assets/icon/right.svg                             |    1 
 src/fabric-editor/assets/icon/layer/image.svg                       |    1 
 src/fabric-editor/assets/icon/layer/polygon.svg                     |    1 
 src/fabric-editor/components/myMaterial/uploadMaterial.vue          |  149 
 src/fabric-editor/assets/filters/Invert.png                         |    0 
 src/fabric-editor/components/attributeBorder.vue                    |  245 
 src/fabric-editor/components/cropperImg.vue                         |   89 
 src/fabric-editor/components/tools.vue                              |  282 +
 src/fabric-editor/assets/icon/tools/text.svg                        |   12 
 src/fabric-editor/components/color-picker/comps/Tabs.vue            |  130 
 src/fabric-editor/components/saveV2.vue                             |   29 
 src/fabric-editor/assets/icon/barcode/center.svg                    |    1 
 src/fabric-editor/components/myMaterial/index.vue                   |   49 
 src/fabric-editor/components/attribute.vue                          |  255 
 types/global.d.ts                                                   |    3 
 src/fabric-editor/assets/icon/flip/x.svg                            |   22 
 src/fabric-editor/assets/filters/Kodachrome.png                     |    0 
 src/services/api/typings.d.ts                                       |  199 
 src/fabric-editor/components/attributeRounded.vue                   |  126 
 src/fabric-editor/components/filters.vue                            |  310 +
 src/fabric-editor/components/myMaterial/myTempl.vue                 |  253 
 src/fabric-editor/assets/icon/tools/circle.svg                      |   14 
 src/fabric-editor/assets/icon/left.svg                              |    1 
 src/fabric-editor/assets/icon/attribute/underline.svg               |    6 
 src/fabric-editor/assets/icon/tools/barCode.svg                     |   15 
 src/fabric-editor/assets/icon/align/averageY.svg                    |    6 
 src/fabric-editor/assets/icon/tools/polygon.svg                     |   14 
 src/fabric-editor/assets/icon/tools/draw1.svg                       |   20 
 src/fabric-editor/assets/icon/centery.svg                           |    1 
 src/fabric-editor/assets/icon/tools/textBox.svg                     |   12 
 202 files changed, 16,188 insertions(+), 62 deletions(-)

diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json
index dde625c..4dc8632 100644
--- a/.eslintrc-auto-import.json
+++ b/.eslintrc-auto-import.json
@@ -29,7 +29,9 @@
     "EnumContractTemplateStatusText": true,
     "EnumContractTemplateStatusTextForEnterpriseFilter": true,
     "EnumContractTemplateValueRecorder": true,
+    "EnumContractTemplateValueRecorderText": true,
     "EnumContractTemplateValueType": true,
+    "EnumContractTemplateValueTypeText": true,
     "EnumDataSource": true,
     "EnumDbAuditOperate": true,
     "EnumDbAuditOperateText": true,
@@ -44,6 +46,7 @@
     "EnumPersonalFreeTime": true,
     "EnumPersonalJobSeekingStatus": true,
     "EnumPersonalRealMethod": true,
+    "EnumPersonalUserRealStatus": true,
     "EnumRealAccess": true,
     "EnumRealAccessText": true,
     "EnumResourceController": true,
@@ -132,6 +135,7 @@
     "SubModuleKey": true,
     "SubModuleTitle": true,
     "SubModuleType": true,
+    "TemplateEditDataItem": true,
     "ThemeColorItem": true,
     "ThemeColorName": true,
     "ThemeColors": true,
diff --git a/auto-imports.d.ts b/auto-imports.d.ts
index b9623fd..17e4ef2 100644
--- a/auto-imports.d.ts
+++ b/auto-imports.d.ts
@@ -37,7 +37,9 @@
   const EnumContractTemplateStatusText: typeof import('./src/constants/electronSign')['EnumContractTemplateStatusText']
   const EnumContractTemplateStatusTextForEnterpriseFilter: typeof import('./src/constants/electronSign')['EnumContractTemplateStatusTextForEnterpriseFilter']
   const EnumContractTemplateValueRecorder: typeof import('./src/constants/apiEnum')['EnumContractTemplateValueRecorder']
+  const EnumContractTemplateValueRecorderText: typeof import('./src/constants/electronSign')['EnumContractTemplateValueRecorderText']
   const EnumContractTemplateValueType: typeof import('./src/constants/apiEnum')['EnumContractTemplateValueType']
+  const EnumContractTemplateValueTypeText: typeof import('./src/constants/electronSign')['EnumContractTemplateValueTypeText']
   const EnumDataSource: typeof import('./src/constants/apiEnum')['EnumDataSource']
   const EnumDbAuditOperate: typeof import('./src/constants/apiEnum')['EnumDbAuditOperate']
   const EnumDbAuditOperateText: typeof import('./src/constants/apiEnumText')['EnumDbAuditOperateText']
@@ -52,6 +54,7 @@
   const EnumPersonalFreeTime: typeof import('./src/constants/apiEnum')['EnumPersonalFreeTime']
   const EnumPersonalJobSeekingStatus: typeof import('./src/constants/apiEnum')['EnumPersonalJobSeekingStatus']
   const EnumPersonalRealMethod: typeof import('./src/constants/apiEnum')['EnumPersonalRealMethod']
+  const EnumPersonalUserRealStatus: typeof import('./src/constants/apiEnum')['EnumPersonalUserRealStatus']
   const EnumRealAccess: typeof import('./src/constants/apiEnum')['EnumRealAccess']
   const EnumRealAccessText: typeof import('./src/constants/enterprise')['EnumRealAccessText']
   const EnumResourceController: typeof import('./src/constants/apiEnum')['EnumResourceController']
@@ -268,7 +271,7 @@
   export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
   // @ts-ignore
-  export type { EnumBillingMethod, EnumClientType, EnumContractTemplateStatus, EnumContractTemplateValueRecorder, EnumContractTemplateValueType, EnumDataSource, EnumDbAuditOperate, EnumElectronSignAccess, EnumEnterpriseRealMethod, EnumMenuType, EnumMenuVisitLevel, EnumOcrAccess, EnumPagedListOrder, EnumPayAccess, EnumPersonalFreeTime, EnumPersonalJobSeekingStatus, EnumPersonalRealMethod, EnumRealAccess, EnumResourceController, EnumResourceMethod, EnumRoleWebApiDataPower, EnumSettlementCycle, EnumSmsAccess, EnumTaskCheckReceiveStatus, EnumTaskRecommendStatus, EnumTaskReleaseStatus, EnumTaskSettlementStatus, EnumTaskStatus, EnumTaskUserArrangeStatus, EnumTaskUserHireStatus, EnumTaskUserSignContractStatus, EnumTaskUserSubmitCheckReceiveStatus, EnumUserGender, EnumUserRealMethod, EnumUserStatus, EnumUserType, GetPersonalApplyTaskInfosQueryStatus, GetPersonalHireTaskInfosQueryStatus, GetTaskInfoQueryResultApplyButton, GetTaskInfoQueryResultHireButton, EnumBillingMethod, EnumClientType, EnumContractTemplateStatus, EnumContractTemplateValueRecorder, EnumContractTemplateValueType, EnumDataSource, EnumDbAuditOperate, EnumElectronSignAccess, EnumEnterpriseRealMethod, EnumMenuType, EnumMenuVisitLevel, EnumOcrAccess, EnumPagedListOrder, EnumPayAccess, EnumPersonalFreeTime, EnumPersonalJobSeekingStatus, EnumPersonalRealMethod, EnumRealAccess, EnumResourceController, EnumResourceMethod, EnumRoleWebApiDataPower, EnumSettlementCycle, EnumSmsAccess, EnumTaskCheckReceiveStatus, EnumTaskRecommendStatus, EnumTaskReleaseStatus, EnumTaskSettlementStatus, EnumTaskStatus, EnumTaskUserArrangeStatus, EnumTaskUserHireStatus, EnumTaskUserSignContractStatus, EnumTaskUserSubmitCheckReceiveStatus, EnumUserGender, EnumUserRealMethod, EnumUserStatus, EnumUserType, GetPersonalApplyTaskInfosQueryStatus, GetPersonalHireTaskInfosQueryStatus, GetTaskInfoQueryResultApplyButton, GetTaskInfoQueryResultHireButton } from './src/constants/apiEnum'
+  export type { EnumBillingMethod, EnumClientType, EnumContractTemplateStatus, EnumContractTemplateValueRecorder, EnumContractTemplateValueType, EnumDataSource, EnumDbAuditOperate, EnumElectronSignAccess, EnumEnterpriseRealMethod, EnumMenuType, EnumMenuVisitLevel, EnumOcrAccess, EnumPagedListOrder, EnumPayAccess, EnumPersonalFreeTime, EnumPersonalJobSeekingStatus, EnumPersonalRealMethod, EnumPersonalUserRealStatus, EnumRealAccess, EnumResourceController, EnumResourceMethod, EnumRoleWebApiDataPower, EnumSettlementCycle, EnumSmsAccess, EnumTaskCheckReceiveStatus, EnumTaskRecommendStatus, EnumTaskReleaseStatus, EnumTaskSettlementStatus, EnumTaskStatus, EnumTaskUserArrangeStatus, EnumTaskUserHireStatus, EnumTaskUserSignContractStatus, EnumTaskUserSubmitCheckReceiveStatus, EnumUserGender, EnumUserRealMethod, EnumUserStatus, EnumUserType, GetPersonalApplyTaskInfosQueryStatus, GetPersonalHireTaskInfosQueryStatus, GetTaskInfoQueryResultApplyButton, GetTaskInfoQueryResultHireButton, EnumBillingMethod, EnumClientType, EnumContractTemplateStatus, EnumContractTemplateValueRecorder, EnumContractTemplateValueType, EnumDataSource, EnumDbAuditOperate, EnumElectronSignAccess, EnumEnterpriseRealMethod, EnumMenuType, EnumMenuVisitLevel, EnumOcrAccess, EnumPagedListOrder, EnumPayAccess, EnumPersonalFreeTime, EnumPersonalJobSeekingStatus, EnumPersonalRealMethod, EnumPersonalUserRealStatus, EnumRealAccess, EnumResourceController, EnumResourceMethod, EnumRoleWebApiDataPower, EnumSettlementCycle, EnumSmsAccess, EnumTaskCheckReceiveStatus, EnumTaskRecommendStatus, EnumTaskReleaseStatus, EnumTaskSettlementStatus, EnumTaskStatus, EnumTaskUserArrangeStatus, EnumTaskUserHireStatus, EnumTaskUserSignContractStatus, EnumTaskUserSubmitCheckReceiveStatus, EnumUserGender, EnumUserRealMethod, EnumUserStatus, EnumUserType, GetPersonalApplyTaskInfosQueryStatus, GetPersonalHireTaskInfosQueryStatus, GetTaskInfoQueryResultApplyButton, GetTaskInfoQueryResultHireButton } from './src/constants/apiEnum'
   import('./src/constants/apiEnum')
   // @ts-ignore
   export type { FlexWorkerEleSignEnum, FlexTaskWorkerHireEnum, FlexWorkerEleSignEnum, FlexTaskWorkerHireEnum } from './src/constants/cPerson'
@@ -279,6 +282,9 @@
   // @ts-ignore
   export type { EditorType, EditorType } from './src/constants/editor'
   import('./src/constants/editor')
+  // @ts-ignore
+  export type { TemplateEditDataItem } from './src/constants/electronSign'
+  import('./src/constants/electronSign')
   // @ts-ignore
   export type { EnterpriseConfigureType, VerifyStatus, FlexEnterpriseCertificationStatus, ChargeTypeEnum, EnterpriseConfigureType, VerifyStatus, FlexEnterpriseCertificationStatus, ChargeTypeEnum } from './src/constants/enterprise'
   import('./src/constants/enterprise')
@@ -335,7 +341,9 @@
     readonly EnumContractTemplateStatusText: UnwrapRef<typeof import('./src/constants/electronSign')['EnumContractTemplateStatusText']>
     readonly EnumContractTemplateStatusTextForEnterpriseFilter: UnwrapRef<typeof import('./src/constants/electronSign')['EnumContractTemplateStatusTextForEnterpriseFilter']>
     readonly EnumContractTemplateValueRecorder: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumContractTemplateValueRecorder']>
+    readonly EnumContractTemplateValueRecorderText: UnwrapRef<typeof import('./src/constants/electronSign')['EnumContractTemplateValueRecorderText']>
     readonly EnumContractTemplateValueType: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumContractTemplateValueType']>
+    readonly EnumContractTemplateValueTypeText: UnwrapRef<typeof import('./src/constants/electronSign')['EnumContractTemplateValueTypeText']>
     readonly EnumDataSource: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumDataSource']>
     readonly EnumDbAuditOperate: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumDbAuditOperate']>
     readonly EnumDbAuditOperateText: UnwrapRef<typeof import('./src/constants/apiEnumText')['EnumDbAuditOperateText']>
@@ -350,6 +358,7 @@
     readonly EnumPersonalFreeTime: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumPersonalFreeTime']>
     readonly EnumPersonalJobSeekingStatus: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumPersonalJobSeekingStatus']>
     readonly EnumPersonalRealMethod: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumPersonalRealMethod']>
+    readonly EnumPersonalUserRealStatus: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumPersonalUserRealStatus']>
     readonly EnumRealAccess: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumRealAccess']>
     readonly EnumRealAccessText: UnwrapRef<typeof import('./src/constants/enterprise')['EnumRealAccessText']>
     readonly EnumResourceController: UnwrapRef<typeof import('./src/constants/apiEnum')['EnumResourceController']>
diff --git a/index.html b/index.html
index e19dc37..cb5e9d0 100644
--- a/index.html
+++ b/index.html
@@ -9,6 +9,7 @@
     <meta name="referrer" content="no-referrer" />
     <title><%- title %></title>
     <link rel="stylesheet" href="<%- subMenuIconCssPath %>" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
     <script>
       window.process = {};
     </script>
diff --git a/package.json b/package.json
index d4b32ec..1e030ca 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "@chenfengyuan/vue-qrcode": "^2.0.0",
     "@ctrl/tinycolor": "^3.4.1",
     "@element-plus/icons-vue": "^2.3.1",
+    "@muaitu/fabric-editor-core": "^1.0.4",
     "@tanstack/vue-query": "^4.37.1",
     "@tencentcloud/call-uikit-vue": "^3.1.5",
     "@tencentcloud/chat-uikit-vue": "^2.0.0",
@@ -48,6 +49,8 @@
     "echarts": "^5.5.1",
     "element-plus": "^2.8.4",
     "element-resize-detector": "^1.2.4",
+    "fabric": "^5.3.0",
+    "fabric-history": "^1.6.0",
     "file-saver": "^2.0.5",
     "fuse.js": "^6.6.2",
     "js-base64": "^3.7.2",
@@ -88,6 +91,7 @@
     "@iconify/vue": "^4.0.0",
     "@release-it/conventional-changelog": "^8.0.2",
     "@types/ali-oss": "^6.16.11",
+    "@types/fabric": "^5.3.10",
     "@types/file-saver": "^2.0.5",
     "@types/js-cookie": "^3.0.2",
     "@types/lodash": "^4.14.186",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index acc4e24..b296cc1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,6 +43,9 @@
   '@element-plus/icons-vue':
     specifier: ^2.3.1
     version: 2.3.1(vue@3.5.11)
+  '@muaitu/fabric-editor-core':
+    specifier: ^1.0.4
+    version: 1.0.4(@webtoon/psd@0.4.0)(events@3.3.0)(fabric-history@1.6.0)(fabric@5.3.0)(fontfaceobserver@2.1.0)(hotkeys-js@3.8.8)(jsbarcode@3.11.6)(qr-code-styling@1.6.0-rc.1)(qs@6.11.0)(tapable@2.2.2)(uuid@8.3.2)
   '@tanstack/vue-query':
     specifier: ^4.37.1
     version: 4.37.1(vue@3.5.11)
@@ -97,6 +100,12 @@
   element-resize-detector:
     specifier: ^1.2.4
     version: 1.2.4
+  fabric:
+    specifier: ^5.3.0
+    version: 5.3.0
+  fabric-history:
+    specifier: ^1.6.0
+    version: 1.6.0
   file-saver:
     specifier: ^2.0.5
     version: 2.0.5
@@ -213,6 +222,9 @@
   '@types/ali-oss':
     specifier: ^6.16.11
     version: 6.16.11
+  '@types/fabric':
+    specifier: ^5.3.10
+    version: 5.3.10
   '@types/file-saver':
     specifier: ^2.0.5
     version: 2.0.5
@@ -2928,7 +2940,7 @@
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.4
+      debug: 4.3.7
       espree: 9.4.0
       globals: 13.17.0
       ignore: 5.3.1
@@ -3092,6 +3104,26 @@
       '@jridgewell/sourcemap-codec': 1.5.0
     dev: true
 
+  /@mapbox/node-pre-gyp@1.0.11:
+    resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
+    hasBin: true
+    requiresBuild: true
+    dependencies:
+      detect-libc: 2.0.4
+      https-proxy-agent: 5.0.1
+      make-dir: 3.1.0
+      node-fetch: 2.7.0
+      nopt: 5.0.0
+      npmlog: 5.0.1
+      rimraf: 3.0.2
+      semver: 7.6.3
+      tar: 6.2.1
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+    dev: false
+    optional: true
+
   /@mdn/browser-compat-data@3.3.14:
     resolution: {integrity: sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==}
     dev: false
@@ -3102,6 +3134,34 @@
     dependencies:
       call-me-maybe: 1.0.2
       glob-to-regexp: 0.3.0
+    dev: false
+
+  /@muaitu/fabric-editor-core@1.0.4(@webtoon/psd@0.4.0)(events@3.3.0)(fabric-history@1.6.0)(fabric@5.3.0)(fontfaceobserver@2.1.0)(hotkeys-js@3.8.8)(jsbarcode@3.11.6)(qr-code-styling@1.6.0-rc.1)(qs@6.11.0)(tapable@2.2.2)(uuid@8.3.2):
+    resolution: {integrity: sha512-OdMcmGx7517HRB8STLBVn9+tjk0RrK5nycYJkZRILZjYljWpbDvD7SBrcXT1zvvU8ppjqfc5t0PLhCScK0tjtA==}
+    peerDependencies:
+      '@webtoon/psd': 0.4.0
+      events: 3.3.0
+      fabric: 5.3.0
+      fabric-history: 1.6.0
+      fontfaceobserver: 2.1.0
+      hotkeys-js: 3.8.8
+      jsbarcode: 3.11.6
+      qr-code-styling: 1.6.0-rc.1
+      qs: '*'
+      tapable: '*'
+      uuid: 8.3.2
+    dependencies:
+      '@webtoon/psd': 0.4.0
+      events: 3.3.0
+      fabric: 5.3.0
+      fabric-history: 1.6.0
+      fontfaceobserver: 2.1.0
+      hotkeys-js: 3.8.8
+      jsbarcode: 3.11.6
+      qr-code-styling: 1.6.0-rc.1
+      qs: 6.11.0
+      tapable: 2.2.2
+      uuid: 8.3.2
     dev: false
 
   /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1:
@@ -3756,6 +3816,13 @@
       '@tiptap/pm': 2.1.13
     dev: false
 
+  /@tootallnate/once@2.0.0:
+    resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
+    engines: {node: '>= 10'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /@tootallnate/quickjs-emscripten@0.23.0:
     resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
     dev: true
@@ -3802,6 +3869,10 @@
   /@types/event-emitter@0.3.5:
     resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
     dev: false
+
+  /@types/fabric@5.3.10:
+    resolution: {integrity: sha512-fsJIuVkU+B2AnmQh+Ml2X0ax3NmRIqLvEXmZ+squX60HaF89TvdIP6tI6Uk5srXaauswTwPOOfWE7k2QboUZCg==}
+    dev: true
 
   /@types/file-saver@2.0.5:
     resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
@@ -5016,6 +5087,10 @@
       snabbdom: 3.6.2
     dev: false
 
+  /@webtoon/psd@0.4.0:
+    resolution: {integrity: sha512-ztriE8oFOamRrV9opBURDy+JMiyhur2//vOXsC5CgdnYCB0L1Lnaag4NzP8N+NFCj7uNz9JRYtPmAbQMSDLIsQ==}
+    dev: false
+
   /@xmldom/xmldom@0.7.6:
     resolution: {integrity: sha512-HHXP9hskkFQHy8QxxUXkS7946FFIhYVfGqsk0WLwllmexN9x/+R4UBLvurHEuyXRfVEObVR8APuQehykLviwSQ==}
     engines: {node: '>=10.0.0'}
@@ -5044,6 +5119,7 @@
       tiny-pinyin: 1.3.2
     transitivePeerDependencies:
       - debug
+      - encoding
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - postcss-jsx
@@ -5063,6 +5139,37 @@
     resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==}
     dev: false
 
+  /abab@2.0.6:
+    resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
+    deprecated: Use your platform's native atob() and btoa() methods instead
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /abbrev@1.1.1:
+    resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /acorn-globals@4.3.4:
+    resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==}
+    requiresBuild: true
+    dependencies:
+      acorn: 6.4.2
+      acorn-walk: 6.2.0
+    dev: false
+    optional: true
+
+  /acorn-globals@6.0.0:
+    resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==}
+    requiresBuild: true
+    dependencies:
+      acorn: 7.4.1
+      acorn-walk: 7.2.0
+    dev: false
+    optional: true
+
   /acorn-jsx@5.3.2(acorn@7.4.1):
     resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
     peerDependencies:
@@ -5071,17 +5178,39 @@
       acorn: 7.4.1
     dev: false
 
-  /acorn-jsx@5.3.2(acorn@8.12.1):
+  /acorn-jsx@5.3.2(acorn@8.15.0):
     resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
     peerDependencies:
       acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      acorn: 8.12.1
+      acorn: 8.15.0
+
+  /acorn-walk@6.2.0:
+    resolution: {integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==}
+    engines: {node: '>=0.4.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /acorn-walk@7.2.0:
+    resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
+    engines: {node: '>=0.4.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /acorn-walk@8.2.0:
     resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
     engines: {node: '>=0.4.0'}
     dev: true
+
+  /acorn@6.4.2:
+    resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /acorn@7.4.1:
     resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
@@ -5093,12 +5222,12 @@
     resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
+    dev: true
 
   /acorn@8.15.0:
     resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
     engines: {node: '>=0.4.0'}
     hasBin: true
-    dev: true
 
   /add-stream@1.0.0:
     resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
@@ -5122,6 +5251,17 @@
       global: 4.4.0
       pkcs7: 1.0.4
     dev: false
+
+  /agent-base@6.0.2:
+    resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+    engines: {node: '>= 6.0.0'}
+    requiresBuild: true
+    dependencies:
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+    optional: true
 
   /agent-base@7.1.1:
     resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
@@ -5230,6 +5370,7 @@
   /ansi-regex@5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
+    requiresBuild: true
 
   /ansi-regex@6.0.1:
     resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
@@ -5268,6 +5409,23 @@
     dependencies:
       normalize-path: 3.0.0
       picomatch: 2.3.1
+
+  /aproba@2.1.0:
+    resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /are-we-there-yet@2.0.0:
+    resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
+    engines: {node: '>=10'}
+    deprecated: This package is no longer supported.
+    requiresBuild: true
+    dependencies:
+      delegates: 1.0.0
+      readable-stream: 3.6.0
+    dev: false
+    optional: true
 
   /arg@4.1.3:
     resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@@ -5311,6 +5469,12 @@
     resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==}
     engines: {node: '>=8'}
     dev: false
+
+  /array-equal@1.0.2:
+    resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /array-find-index@1.0.2:
     resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==}
@@ -5443,6 +5607,21 @@
     resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
     dev: false
 
+  /asn1@0.2.6:
+    resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
+    requiresBuild: true
+    dependencies:
+      safer-buffer: 2.1.2
+    dev: false
+    optional: true
+
+  /assert-plus@1.0.0:
+    resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
+    engines: {node: '>=0.8'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /assign-symbols@1.0.0:
     resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
     engines: {node: '>=0.10.0'}
@@ -5489,6 +5668,7 @@
 
   /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+    requiresBuild: true
     dev: false
 
   /atob@2.1.2:
@@ -5553,6 +5733,18 @@
     engines: {node: '>= 0.4'}
     dependencies:
       possible-typed-array-names: 1.0.0
+
+  /aws-sign2@0.7.0:
+    resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /aws4@1.13.2:
+    resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /axe-core@4.7.0:
     resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==}
@@ -5647,6 +5839,14 @@
     resolution: {integrity: sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==}
     dev: false
 
+  /bcrypt-pbkdf@1.0.2:
+    resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
+    requiresBuild: true
+    dependencies:
+      tweetnacl: 0.14.5
+    dev: false
+    optional: true
+
   /before-after-hook@2.2.3:
     resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
     dev: true
@@ -5695,6 +5895,7 @@
 
   /brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+    requiresBuild: true
     dependencies:
       balanced-match: 1.0.2
       concat-map: 0.0.1
@@ -5727,6 +5928,12 @@
     engines: {node: '>=8'}
     dependencies:
       fill-range: 7.0.1
+
+  /browser-process-hrtime@1.0.0:
+    resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /browserslist-to-esbuild@2.1.1(browserslist@4.24.0):
     resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==}
@@ -5917,6 +6124,20 @@
   /caniuse-lite@1.0.30001667:
     resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==}
 
+  /canvas@2.11.2:
+    resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==}
+    engines: {node: '>=6'}
+    requiresBuild: true
+    dependencies:
+      '@mapbox/node-pre-gyp': 1.0.11
+      nan: 2.23.0
+      simple-get: 3.1.1
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+    dev: false
+    optional: true
+
   /capital-case@1.0.4:
     resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
     dependencies:
@@ -5929,6 +6150,12 @@
     resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
     engines: {node: '>=12.13'}
     dev: false
+
+  /caseless@0.12.0:
+    resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /ccount@1.1.0:
     resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
@@ -6038,6 +6265,13 @@
       readdirp: 3.6.0
     optionalDependencies:
       fsevents: 2.3.3
+
+  /chownr@2.0.0:
+    resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
+    engines: {node: '>=10'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /ci-info@2.0.0:
     resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
@@ -6226,6 +6460,13 @@
       color-name: 1.1.4
     dev: false
 
+  /color-support@1.1.3:
+    resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
+    hasBin: true
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /color@0.11.4:
     resolution: {integrity: sha512-Ajpjd8asqZ6EdxQeqGzU5WBhhTfJ/0cA4Wlbre7e5vXfmDSmda7Ov6jeKoru+b0vHcb1CqvuroTHp5zIWzhVMA==}
     dependencies:
@@ -6250,6 +6491,7 @@
   /combined-stream@1.0.8:
     resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
     engines: {node: '>= 0.8'}
+    requiresBuild: true
     dependencies:
       delayed-stream: 1.0.0
     dev: false
@@ -6301,6 +6543,7 @@
 
   /concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+    requiresBuild: true
 
   /concat-stream@2.0.0:
     resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
@@ -6360,6 +6603,12 @@
   /consola@2.15.3:
     resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
     dev: true
+
+  /console-control-strings@1.1.0:
+    resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /console@0.7.2:
     resolution: {integrity: sha512-+JSDwGunA4MTEgAV/4VBKwUHonP8CzJ/6GIuwPi6acKFqFfHUdSGCm89ZxZ5FfGWdZfkdgAroy5bJ5FSeN/t4g==}
@@ -6558,6 +6807,12 @@
   /core-js@3.38.1:
     resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
     requiresBuild: true
+
+  /core-util-is@1.0.2:
+    resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /core-util-is@1.0.3:
     resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -6770,6 +7025,33 @@
       css-tree: 1.1.3
     dev: true
 
+  /cssom@0.3.8:
+    resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /cssom@0.4.4:
+    resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /cssom@0.5.0:
+    resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /cssstyle@2.3.0:
+    resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dependencies:
+      cssom: 0.3.8
+    dev: false
+    optional: true
+
   /csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
@@ -6805,10 +7087,40 @@
     resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
     dev: false
 
+  /dashdash@1.14.1:
+    resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
+    engines: {node: '>=0.10'}
+    requiresBuild: true
+    dependencies:
+      assert-plus: 1.0.0
+    dev: false
+    optional: true
+
   /data-uri-to-buffer@6.0.2:
     resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
     engines: {node: '>= 14'}
     dev: true
+
+  /data-urls@1.1.0:
+    resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==}
+    requiresBuild: true
+    dependencies:
+      abab: 2.0.6
+      whatwg-mimetype: 2.3.0
+      whatwg-url: 7.1.0
+    dev: false
+    optional: true
+
+  /data-urls@3.0.2:
+    resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      abab: 2.0.6
+      whatwg-mimetype: 3.0.0
+      whatwg-url: 11.0.0
+    dev: false
+    optional: true
 
   /data-view-buffer@1.0.1:
     resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
@@ -6908,9 +7220,24 @@
     resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
     engines: {node: '>=0.10.0'}
 
+  /decimal.js@10.6.0:
+    resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /decode-uri-component@0.2.0:
     resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==}
     engines: {node: '>=0.10'}
+
+  /decompress-response@4.2.1:
+    resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dependencies:
+      mimic-response: 2.1.0
+    dev: false
+    optional: true
 
   /deep-extend@0.6.0:
     resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
@@ -7008,11 +7335,18 @@
   /delayed-stream@1.0.0:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
+    requiresBuild: true
     dev: false
 
   /delegate@3.2.0:
     resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
     dev: false
+
+  /delegates@1.0.0:
+    resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /deprecation@2.3.1:
     resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
@@ -7027,6 +7361,13 @@
     resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
     dev: false
+
+  /detect-libc@2.0.4:
+    resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /diff@4.0.2:
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
@@ -7124,6 +7465,25 @@
   /domelementtype@2.3.0:
     resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
 
+  /domexception@1.0.1:
+    resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==}
+    deprecated: Use your platform's native DOMException instead
+    requiresBuild: true
+    dependencies:
+      webidl-conversions: 4.0.2
+    dev: false
+    optional: true
+
+  /domexception@4.0.0:
+    resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
+    engines: {node: '>=12'}
+    deprecated: Use your platform's native DOMException instead
+    requiresBuild: true
+    dependencies:
+      webidl-conversions: 7.0.0
+    dev: false
+    optional: true
+
   /domhandler@2.4.2:
     resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==}
     dependencies:
@@ -7203,6 +7563,15 @@
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     dev: true
 
+  /ecc-jsbn@0.1.2:
+    resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
+    requiresBuild: true
+    dependencies:
+      jsbn: 0.1.1
+      safer-buffer: 2.1.2
+    dev: false
+    optional: true
+
   /echarts@5.5.1:
     resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
     dependencies:
@@ -7273,6 +7642,7 @@
 
   /emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+    requiresBuild: true
 
   /emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
@@ -7541,6 +7911,21 @@
     engines: {node: '>=12'}
     dev: true
 
+  /escodegen@1.14.3:
+    resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==}
+    engines: {node: '>=4.0'}
+    hasBin: true
+    requiresBuild: true
+    dependencies:
+      esprima: 4.0.1
+      estraverse: 4.3.0
+      esutils: 2.0.3
+      optionator: 0.8.3
+    optionalDependencies:
+      source-map: 0.6.1
+    dev: false
+    optional: true
+
   /escodegen@2.1.0:
     resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
     engines: {node: '>=6.0'}
@@ -7551,7 +7936,6 @@
       esutils: 2.0.3
     optionalDependencies:
       source-map: 0.6.1
-    dev: true
 
   /eslint-ast-utils@1.1.0:
     resolution: {integrity: sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==}
@@ -8093,8 +8477,8 @@
     resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
-      acorn: 8.12.1
-      acorn-jsx: 5.3.2(acorn@8.12.1)
+      acorn: 8.15.0
+      acorn-jsx: 5.3.2(acorn@8.15.0)
       eslint-visitor-keys: 3.3.0
 
   /esprima@4.0.1:
@@ -8149,6 +8533,11 @@
 
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+    dev: false
+
+  /events@3.3.0:
+    resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+    engines: {node: '>=0.8.x'}
     dev: false
 
   /execa@5.1.1:
@@ -8274,6 +8663,50 @@
       to-regex: 3.0.2
     transitivePeerDependencies:
       - supports-color
+
+  /extsprintf@1.3.0:
+    resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
+    engines: {'0': node >=0.6.0}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /fabric-history@1.6.0:
+    resolution: {integrity: sha512-a2njIpTgmtwcFliCRSC9t0uy1sUwSmGShvhxUO5H19uFzLgCuLivlxleXUIYjGC5QQmWQE7ooEa3/KETxtuLhA==}
+    dependencies:
+      fabric: 3.6.6
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: false
+
+  /fabric@3.6.6:
+    resolution: {integrity: sha512-+sEwsgLSLIHQ32INuvV4CvWtqYOLyDKify/FfDteZvYQSoWRILLqzpCRt6AkEySsLToohkcygkD/unEOzV38wQ==}
+    engines: {node: '>=8.0.0'}
+    optionalDependencies:
+      canvas: 2.11.2
+      jsdom: 15.2.1(canvas@2.11.2)
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: false
+
+  /fabric@5.3.0:
+    resolution: {integrity: sha512-AVayKuzWoXM5cTn7iD3yNWBlfEa8r1tHaOe2g8NsZrmWKAHjryTxT/j6f9ncRfOWOF0I1Ci1AId3y78cC+GExQ==}
+    engines: {node: '>=14.0.0'}
+    optionalDependencies:
+      canvas: 2.11.2
+      jsdom: 19.0.0(canvas@2.11.2)
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: false
 
   /fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -8462,6 +8895,10 @@
         optional: true
     dev: false
 
+  /fontfaceobserver@2.1.0:
+    resolution: {integrity: sha512-ReOsO2F66jUa0jmv2nlM/s1MiutJx/srhAe2+TE8dJCMi02ZZOcCTxTCQFr3Yet+uODUtnr4Mewg+tNQ+4V1Ng==}
+    dev: false
+
   /for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
     dependencies:
@@ -8478,6 +8915,23 @@
       cross-spawn: 7.0.3
       signal-exit: 4.1.0
     dev: true
+
+  /forever-agent@0.6.1:
+    resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /form-data@2.3.3:
+    resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
+    engines: {node: '>= 0.12'}
+    requiresBuild: true
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+    dev: false
+    optional: true
 
   /form-data@4.0.0:
     resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
@@ -8544,6 +8998,15 @@
       universalify: 2.0.0
     dev: true
 
+  /fs-minipass@2.1.0:
+    resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
+    engines: {node: '>= 8'}
+    requiresBuild: true
+    dependencies:
+      minipass: 3.3.6
+    dev: false
+    optional: true
+
   /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
@@ -8577,6 +9040,24 @@
     resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
     engines: {node: '>=10'}
     dev: false
+
+  /gauge@3.0.2:
+    resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
+    engines: {node: '>=10'}
+    deprecated: This package is no longer supported.
+    requiresBuild: true
+    dependencies:
+      aproba: 2.1.0
+      color-support: 1.1.3
+      console-control-strings: 1.1.0
+      has-unicode: 2.0.1
+      object-assign: 4.1.1
+      signal-exit: 3.0.7
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wide-align: 1.1.5
+    dev: false
+    optional: true
 
   /gensync@1.0.0-beta.2:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
@@ -8648,6 +9129,14 @@
   /get-value@2.0.6:
     resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==}
     engines: {node: '>=0.10.0'}
+
+  /getpass@0.1.7:
+    resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
+    requiresBuild: true
+    dependencies:
+      assert-plus: 1.0.0
+    dev: false
+    optional: true
 
   /git-raw-commits@2.0.11:
     resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
@@ -8904,6 +9393,24 @@
       uglify-js: 3.18.0
     dev: true
 
+  /har-schema@2.0.0:
+    resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
+    engines: {node: '>=4'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /har-validator@5.1.5:
+    resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==}
+    engines: {node: '>=6'}
+    deprecated: this library is no longer supported
+    requiresBuild: true
+    dependencies:
+      ajv: 6.12.6
+      har-schema: 2.0.0
+    dev: false
+    optional: true
+
   /hard-rejection@2.1.0:
     resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
     engines: {node: '>=6'}
@@ -8949,6 +9456,12 @@
     engines: {node: '>= 0.4'}
     dependencies:
       has-symbols: 1.0.3
+
+  /has-unicode@2.0.1:
+    resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /has-value@0.3.1:
     resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==}
@@ -9015,6 +9528,27 @@
       lru-cache: 10.1.0
     dev: true
 
+  /hotkeys-js@3.8.8:
+    resolution: {integrity: sha512-3vUjgRJiQOHDFouNnr6fKMffGtkujPEnmUutJc1O6NVS/jTDWge5+XDrMKONVg9NGhzl2kY1nhG2/CEnXKFR7A==}
+    dev: false
+
+  /html-encoding-sniffer@1.0.2:
+    resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==}
+    requiresBuild: true
+    dependencies:
+      whatwg-encoding: 1.0.5
+    dev: false
+    optional: true
+
+  /html-encoding-sniffer@3.0.0:
+    resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      whatwg-encoding: 2.0.0
+    dev: false
+    optional: true
+
   /html-minifier-terser@6.1.0:
     resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==}
     engines: {node: '>=12'}
@@ -9065,6 +9599,19 @@
       domutils: 3.0.1
       entities: 4.5.0
 
+  /http-proxy-agent@5.0.0:
+    resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
+    engines: {node: '>= 6'}
+    requiresBuild: true
+    dependencies:
+      '@tootallnate/once': 2.0.0
+      agent-base: 6.0.2
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+    optional: true
+
   /http-proxy-agent@7.0.2:
     resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
     engines: {node: '>= 14'}
@@ -9075,9 +9622,32 @@
       - supports-color
     dev: true
 
+  /http-signature@1.2.0:
+    resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==}
+    engines: {node: '>=0.8', npm: '>=1.3.7'}
+    requiresBuild: true
+    dependencies:
+      assert-plus: 1.0.0
+      jsprim: 1.4.2
+      sshpk: 1.18.0
+    dev: false
+    optional: true
+
   /http2-client@1.3.5:
     resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==}
     dev: false
+
+  /https-proxy-agent@5.0.1:
+    resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+    engines: {node: '>= 6'}
+    requiresBuild: true
+    dependencies:
+      agent-base: 6.0.2
+      debug: 4.3.7
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+    optional: true
 
   /https-proxy-agent@7.0.4:
     resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
@@ -9127,7 +9697,6 @@
     engines: {node: '>=0.10.0'}
     dependencies:
       safer-buffer: 2.1.2
-    dev: true
 
   /iconv-lite@0.6.3:
     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
@@ -9269,6 +9838,13 @@
     resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
     engines: {node: '>= 0.10'}
     dev: true
+
+  /ip-regex@2.1.0:
+    resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==}
+    engines: {node: '>=4'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /ip@2.0.0:
     resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
@@ -9584,6 +10160,12 @@
     resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
     engines: {node: '>=0.10.0'}
 
+  /is-potential-custom-element-name@1.0.1:
+    resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /is-promise@2.2.2:
     resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
     dev: false
@@ -9848,6 +10430,104 @@
     dependencies:
       argparse: 2.0.1
 
+  /jsbarcode@3.11.6:
+    resolution: {integrity: sha512-G5TKGyKY1zJo0ZQKFM1IIMfy0nF2rs92BLlCz+cU4/TazIc4ZH+X1GYeDRt7TKjrYqmPfTjwTBkU/QnQlsYiuA==}
+    dev: false
+
+  /jsbn@0.1.1:
+    resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /jsdom@15.2.1(canvas@2.11.2):
+    resolution: {integrity: sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    peerDependencies:
+      canvas: ^2.5.0
+    peerDependenciesMeta:
+      canvas:
+        optional: true
+    dependencies:
+      abab: 2.0.6
+      acorn: 7.4.1
+      acorn-globals: 4.3.4
+      array-equal: 1.0.2
+      canvas: 2.11.2
+      cssom: 0.4.4
+      cssstyle: 2.3.0
+      data-urls: 1.1.0
+      domexception: 1.0.1
+      escodegen: 1.14.3
+      html-encoding-sniffer: 1.0.2
+      nwsapi: 2.2.21
+      parse5: 5.1.0
+      pn: 1.1.0
+      request: 2.88.2
+      request-promise-native: 1.0.9(request@2.88.2)
+      saxes: 3.1.11
+      symbol-tree: 3.2.4
+      tough-cookie: 3.0.1
+      w3c-hr-time: 1.0.2
+      w3c-xmlserializer: 1.1.2
+      webidl-conversions: 4.0.2
+      whatwg-encoding: 1.0.5
+      whatwg-mimetype: 2.3.0
+      whatwg-url: 7.1.0
+      ws: 7.5.10
+      xml-name-validator: 3.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    dev: false
+    optional: true
+
+  /jsdom@19.0.0(canvas@2.11.2):
+    resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    peerDependencies:
+      canvas: ^2.5.0
+    peerDependenciesMeta:
+      canvas:
+        optional: true
+    dependencies:
+      abab: 2.0.6
+      acorn: 8.15.0
+      acorn-globals: 6.0.0
+      canvas: 2.11.2
+      cssom: 0.5.0
+      cssstyle: 2.3.0
+      data-urls: 3.0.2
+      decimal.js: 10.6.0
+      domexception: 4.0.0
+      escodegen: 2.1.0
+      form-data: 4.0.0
+      html-encoding-sniffer: 3.0.0
+      http-proxy-agent: 5.0.0
+      https-proxy-agent: 5.0.1
+      is-potential-custom-element-name: 1.0.1
+      nwsapi: 2.2.21
+      parse5: 6.0.1
+      saxes: 5.0.1
+      symbol-tree: 3.2.4
+      tough-cookie: 4.1.4
+      w3c-hr-time: 1.0.2
+      w3c-xmlserializer: 3.0.0
+      webidl-conversions: 7.0.0
+      whatwg-encoding: 2.0.0
+      whatwg-mimetype: 3.0.0
+      whatwg-url: 10.0.0
+      ws: 8.18.3
+      xml-name-validator: 4.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+    dev: false
+    optional: true
+
   /jsesc@0.5.0:
     resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
     hasBin: true
@@ -9874,12 +10554,17 @@
   /json-schema-traverse@1.0.0:
     resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
 
+  /json-schema@0.4.0:
+    resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /json-stable-stringify-without-jsonify@1.0.1:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
 
   /json-stringify-safe@5.0.1:
     resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
-    dev: true
 
   /json5@1.0.2:
     resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
@@ -9904,6 +10589,18 @@
     resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
     engines: {'0': node >= 0.2.0}
     dev: true
+
+  /jsprim@1.4.2:
+    resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
+    engines: {node: '>=0.6.0'}
+    requiresBuild: true
+    dependencies:
+      assert-plus: 1.0.0
+      extsprintf: 1.3.0
+      json-schema: 0.4.0
+      verror: 1.10.0
+    dev: false
+    optional: true
 
   /jsrsasign@10.5.27:
     resolution: {integrity: sha512-1F4LmDeJZHYwoVvB44jEo2uZL3XuwYNzXCDOu53Ui6vqofGQ/gCYDmaxfVZtN0TGd92UKXr/BONcfrPonUIcQQ==}
@@ -10012,6 +10709,16 @@
     resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==}
     engines: {node: '>=0.10.0'}
     dev: false
+
+  /levn@0.3.0:
+    resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
+    engines: {node: '>= 0.8.0'}
+    requiresBuild: true
+    dependencies:
+      prelude-ls: 1.1.2
+      type-check: 0.3.2
+    dev: false
+    optional: true
 
   /levn@0.4.1:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
@@ -10227,6 +10934,12 @@
   /lodash.merge@4.6.2:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
 
+  /lodash.sortby@4.7.0:
+    resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /lodash.throttle@4.1.1:
     resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
     dev: false
@@ -10378,6 +11091,15 @@
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.0
     dev: true
+
+  /make-dir@3.1.0:
+    resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dependencies:
+      semver: 6.3.1
+    dev: false
+    optional: true
 
   /make-error@1.3.6:
     resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
@@ -10636,6 +11358,7 @@
   /mime-db@1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
+    requiresBuild: true
 
   /mime-match@1.0.2:
     resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
@@ -10669,6 +11392,13 @@
     resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
     engines: {node: '>=18'}
     dev: true
+
+  /mimic-response@2.1.0:
+    resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /min-document@2.19.0:
     resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
@@ -10725,10 +11455,36 @@
   /minimist@1.2.7:
     resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
 
+  /minipass@3.3.6:
+    resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dependencies:
+      yallist: 4.0.0
+    dev: false
+    optional: true
+
+  /minipass@5.0.0:
+    resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /minipass@7.1.2:
     resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
     engines: {node: '>=16 || 14 >=14.17'}
     dev: true
+
+  /minizlib@2.1.2:
+    resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
+    engines: {node: '>= 8'}
+    requiresBuild: true
+    dependencies:
+      minipass: 3.3.6
+      yallist: 4.0.0
+    dev: false
+    optional: true
 
   /mitt@3.0.0:
     resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==}
@@ -10748,10 +11504,18 @@
       minimist: 1.2.7
     dev: false
 
+  /mkdirp@1.0.4:
+    resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
+    engines: {node: '>=10'}
+    hasBin: true
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /mlly@1.7.2:
     resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==}
     dependencies:
-      acorn: 8.12.1
+      acorn: 8.15.0
       pathe: 1.1.2
       pkg-types: 1.2.1
       ufo: 1.5.4
@@ -10840,6 +11604,12 @@
     resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
     dev: false
 
+  /nan@2.23.0:
+    resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /nanoid@3.3.4:
     resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -10920,6 +11690,19 @@
     engines: {node: 4.x || >=6.0.0}
     dev: false
 
+  /node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    requiresBuild: true
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+    dependencies:
+      whatwg-url: 5.0.0
+    dev: false
+
   /node-html-parser@5.4.2:
     resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==}
     dependencies:
@@ -10943,6 +11726,16 @@
   /node-releases@2.0.6:
     resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
     dev: true
+
+  /nopt@5.0.0:
+    resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
+    engines: {node: '>=6'}
+    hasBin: true
+    requiresBuild: true
+    dependencies:
+      abbrev: 1.1.1
+    dev: false
+    optional: true
 
   /normalize-package-data@2.5.0:
     resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -11017,6 +11810,18 @@
       path-key: 4.0.0
     dev: true
 
+  /npmlog@5.0.1:
+    resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
+    deprecated: This package is no longer supported.
+    requiresBuild: true
+    dependencies:
+      are-we-there-yet: 2.0.0
+      console-control-strings: 1.1.0
+      gauge: 3.0.2
+      set-blocking: 2.0.0
+    dev: false
+    optional: true
+
   /nprogress@0.2.0:
     resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
     dev: false
@@ -11042,6 +11847,12 @@
     optionalDependencies:
       chokidar: 3.6.0
     dev: false
+
+  /nwsapi@2.2.21:
+    resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /oas-kit-common@1.0.8:
     resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
@@ -11084,6 +11895,12 @@
       should: 13.2.3
       yaml: 1.10.2
     dev: false
+
+  /oauth-sign@0.9.0:
+    resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@@ -11239,6 +12056,20 @@
     dependencies:
       yaml: 1.10.2
     dev: false
+
+  /optionator@0.8.3:
+    resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
+    engines: {node: '>= 0.8.0'}
+    requiresBuild: true
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.3.0
+      prelude-ls: 1.1.2
+      type-check: 0.3.2
+      word-wrap: 1.2.3
+    dev: false
+    optional: true
 
   /optionator@0.9.1:
     resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
@@ -11503,6 +12334,18 @@
       parse-path: 7.0.0
     dev: true
 
+  /parse5@5.1.0:
+    resolution: {integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /parse5@6.0.1:
+    resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /parseurl@1.3.3:
     resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
     engines: {node: '>= 0.8'}
@@ -11611,6 +12454,12 @@
     dependencies:
       through: 2.3.8
     dev: false
+
+  /performance-now@2.1.0:
+    resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /picocolors@0.2.1:
     resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
@@ -11721,6 +12570,12 @@
     resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
     engines: {node: '>=4'}
     dev: false
+
+  /pn@1.1.0:
+    resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /pngjs@5.0.0:
     resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
@@ -12401,6 +13256,13 @@
     resolution: {integrity: sha512-O5UdRsNh4vdZaTieWe3XOgSpdMAmkIYBCT3VhQDlKrzyCm8lUYsk0fmVEvoQQifoOjFRTaHZO69ylrzTW2BH+A==}
     dev: false
 
+  /prelude-ls@1.1.2:
+    resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==}
+    engines: {node: '>= 0.8.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /prelude-ls@1.2.1:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
@@ -12631,6 +13493,14 @@
   /proxy-from-env@1.1.0:
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
 
+  /psl@1.15.0:
+    resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
+    requiresBuild: true
+    dependencies:
+      punycode: 2.3.1
+    dev: false
+    optional: true
+
   /pump@3.0.0:
     resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
     dependencies:
@@ -12643,9 +13513,10 @@
     engines: {node: '>=6'}
     dev: false
 
-  /punycode@2.1.1:
-    resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
+  /punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
+    requiresBuild: true
 
   /pupa@3.1.0:
     resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==}
@@ -12658,6 +13529,16 @@
     resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
     engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
     dev: true
+
+  /qr-code-styling@1.6.0-rc.1:
+    resolution: {integrity: sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q==}
+    dependencies:
+      qrcode-generator: 1.5.2
+    dev: false
+
+  /qrcode-generator@1.5.2:
+    resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==}
+    dev: false
 
   /qrcode@1.5.4:
     resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
@@ -12676,6 +13557,13 @@
       side-channel: 1.0.4
     dev: false
 
+  /qs@6.5.3:
+    resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
+    engines: {node: '>=0.6'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /quansync@0.2.10:
     resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
     dev: true
@@ -12687,6 +13575,12 @@
       object-assign: 4.1.1
       strict-uri-encode: 1.1.0
     dev: true
+
+  /querystringify@2.2.0:
+    resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /queue-microtask@1.2.3:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -13105,6 +13999,62 @@
     engines: {node: '>= 0.10'}
     dev: false
 
+  /request-promise-core@1.1.4(request@2.88.2):
+    resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==}
+    engines: {node: '>=0.10.0'}
+    requiresBuild: true
+    peerDependencies:
+      request: ^2.34
+    dependencies:
+      lodash: 4.17.21
+      request: 2.88.2
+    dev: false
+    optional: true
+
+  /request-promise-native@1.0.9(request@2.88.2):
+    resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==}
+    engines: {node: '>=0.12.0'}
+    deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
+    requiresBuild: true
+    peerDependencies:
+      request: ^2.34
+    dependencies:
+      request: 2.88.2
+      request-promise-core: 1.1.4(request@2.88.2)
+      stealthy-require: 1.1.1
+      tough-cookie: 2.5.0
+    dev: false
+    optional: true
+
+  /request@2.88.2:
+    resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==}
+    engines: {node: '>= 6'}
+    deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
+    requiresBuild: true
+    dependencies:
+      aws-sign2: 0.7.0
+      aws4: 1.13.2
+      caseless: 0.12.0
+      combined-stream: 1.0.8
+      extend: 3.0.2
+      forever-agent: 0.6.1
+      form-data: 2.3.3
+      har-validator: 5.1.5
+      http-signature: 1.2.0
+      is-typedarray: 1.0.0
+      isstream: 0.1.2
+      json-stringify-safe: 5.0.1
+      mime-types: 2.1.35
+      oauth-sign: 0.9.0
+      performance-now: 2.1.0
+      qs: 6.5.3
+      safe-buffer: 5.2.1
+      tough-cookie: 2.5.0
+      tunnel-agent: 0.6.0
+      uuid: 3.4.0
+    dev: false
+    optional: true
+
   /require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -13116,6 +14066,12 @@
   /require-main-filename@2.0.0:
     resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
     dev: false
+
+  /requires-port@1.0.0:
+    resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /reserved-words@0.1.2:
     resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==}
@@ -13340,6 +14296,7 @@
 
   /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+    requiresBuild: true
 
   /safe-json-parse@4.0.0:
     resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==}
@@ -13368,6 +14325,7 @@
 
   /safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+    requiresBuild: true
 
   /sass@1.55.0:
     resolution: {integrity: sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==}
@@ -13382,6 +14340,24 @@
   /sax@1.2.4:
     resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
     dev: false
+
+  /saxes@3.1.11:
+    resolution: {integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==}
+    engines: {node: '>=8'}
+    requiresBuild: true
+    dependencies:
+      xmlchars: 2.2.0
+    dev: false
+    optional: true
+
+  /saxes@5.0.1:
+    resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
+    engines: {node: '>=10'}
+    requiresBuild: true
+    dependencies:
+      xmlchars: 2.2.0
+    dev: false
+    optional: true
 
   /scroll-into-view-if-needed@2.2.31:
     resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
@@ -13641,6 +14617,22 @@
     engines: {node: '>=14'}
     dev: true
 
+  /simple-concat@1.0.1:
+    resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /simple-get@3.1.1:
+    resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==}
+    requiresBuild: true
+    dependencies:
+      decompress-response: 4.2.1
+      once: 1.4.0
+      simple-concat: 1.0.1
+    dev: false
+    optional: true
+
   /slash@2.0.0:
     resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
     engines: {node: '>=6'}
@@ -13888,6 +14880,24 @@
       voc: 1.2.0
     dev: false
 
+  /sshpk@1.18.0:
+    resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
+    engines: {node: '>=0.10.0'}
+    hasBin: true
+    requiresBuild: true
+    dependencies:
+      asn1: 0.2.6
+      assert-plus: 1.0.0
+      bcrypt-pbkdf: 1.0.2
+      dashdash: 1.14.1
+      ecc-jsbn: 0.1.2
+      getpass: 0.1.7
+      jsbn: 0.1.1
+      safer-buffer: 2.1.2
+      tweetnacl: 0.14.5
+    dev: false
+    optional: true
+
   /ssr-window@3.0.0:
     resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
     dev: false
@@ -13916,6 +14926,13 @@
     resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
     engines: {node: '>=18'}
     dev: true
+
+  /stealthy-require@1.1.1:
+    resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==}
+    engines: {node: '>=0.10.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /stream-http@2.8.2:
     resolution: {integrity: sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==}
@@ -14041,6 +15058,7 @@
 
   /string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+    requiresBuild: true
     dependencies:
       safe-buffer: 5.2.1
 
@@ -14518,7 +15536,7 @@
     hasBin: true
     dependencies:
       call-me-maybe: 1.0.2
-      node-fetch: 2.6.1
+      node-fetch: 2.7.0
       node-fetch-h2: 2.3.0
       node-readfiles: 0.2.0
       oas-kit-common: 1.0.8
@@ -14528,7 +15546,15 @@
       reftools: 1.1.9
       yaml: 1.10.2
       yargs: 16.2.0
+    transitivePeerDependencies:
+      - encoding
     dev: false
+
+  /symbol-tree@3.2.4:
+    resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /synckit@0.9.2:
     resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
@@ -14561,6 +15587,25 @@
       slice-ansi: 4.0.0
       string-width: 4.2.3
       strip-ansi: 6.0.1
+
+  /tapable@2.2.2:
+    resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
+    engines: {node: '>=6'}
+    dev: false
+
+  /tar@6.2.1:
+    resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
+    engines: {node: '>=10'}
+    requiresBuild: true
+    dependencies:
+      chownr: 2.0.0
+      fs-minipass: 2.1.0
+      minipass: 5.0.0
+      minizlib: 2.1.2
+      mkdirp: 1.0.4
+      yallist: 4.0.0
+    dev: false
+    optional: true
 
   /terser@5.34.1:
     resolution: {integrity: sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==}
@@ -14710,6 +15755,61 @@
       regex-not: 1.0.2
       safe-regex: 1.1.0
 
+  /tough-cookie@2.5.0:
+    resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
+    engines: {node: '>=0.8'}
+    requiresBuild: true
+    dependencies:
+      psl: 1.15.0
+      punycode: 2.3.1
+    dev: false
+    optional: true
+
+  /tough-cookie@3.0.1:
+    resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==}
+    engines: {node: '>=6'}
+    requiresBuild: true
+    dependencies:
+      ip-regex: 2.1.0
+      psl: 1.15.0
+      punycode: 2.3.1
+    dev: false
+    optional: true
+
+  /tough-cookie@4.1.4:
+    resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
+    engines: {node: '>=6'}
+    requiresBuild: true
+    dependencies:
+      psl: 1.15.0
+      punycode: 2.3.1
+      universalify: 0.2.0
+      url-parse: 1.5.10
+    dev: false
+    optional: true
+
+  /tr46@0.0.3:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+    requiresBuild: true
+    dev: false
+
+  /tr46@1.0.1:
+    resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
+    requiresBuild: true
+    dependencies:
+      punycode: 2.3.1
+    dev: false
+    optional: true
+
+  /tr46@3.0.0:
+    resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      punycode: 2.3.1
+    dev: false
+    optional: true
+
   /traverse@0.6.7:
     resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==}
     dev: true
@@ -14783,7 +15883,7 @@
       '@tsconfig/node14': 1.0.3
       '@tsconfig/node16': 1.0.3
       '@types/node': 18.7.18
-      acorn: 8.12.1
+      acorn: 8.15.0
       acorn-walk: 8.2.0
       arg: 4.1.3
       create-require: 1.1.1
@@ -14842,6 +15942,29 @@
   /tuikit-logger@0.0.4-beta.1:
     resolution: {integrity: sha512-Ky83B1p88xakmfZ2f92cU0YxfolyxnQBv14tQpvnuHcMTnVR2Rjy8tityDGwF+pnxrAhJ7H7OPB/4rFdWVncIw==}
     dev: false
+
+  /tunnel-agent@0.6.0:
+    resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+    requiresBuild: true
+    dependencies:
+      safe-buffer: 5.2.1
+    dev: false
+    optional: true
+
+  /tweetnacl@0.14.5:
+    resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /type-check@0.3.2:
+    resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
+    engines: {node: '>= 0.8.0'}
+    requiresBuild: true
+    dependencies:
+      prelude-ls: 1.1.2
+    dev: false
+    optional: true
 
   /type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
@@ -15153,6 +16276,13 @@
     resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
     dev: true
 
+  /universalify@0.2.0:
+    resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
+    engines: {node: '>= 4.0.0'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /universalify@2.0.0:
     resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
     engines: {node: '>= 10.0.0'}
@@ -15253,7 +16383,7 @@
   /unplugin@0.9.6:
     resolution: {integrity: sha512-YYLtfoNiie/lxswy1GOsKXgnLJTE27la/PeCGznSItk+8METYZErO+zzV9KQ/hXhPwzIJsfJ4s0m1Rl7ZCWZ4Q==}
     dependencies:
-      acorn: 8.12.1
+      acorn: 8.15.0
       chokidar: 3.6.0
       webpack-sources: 3.2.3
       webpack-virtual-modules: 0.4.5
@@ -15268,7 +16398,7 @@
       webpack-sources:
         optional: true
     dependencies:
-      acorn: 8.12.1
+      acorn: 8.15.0
       webpack-virtual-modules: 0.6.2
     dev: true
 
@@ -15362,7 +16492,7 @@
   /uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
     dependencies:
-      punycode: 2.1.1
+      punycode: 2.3.1
 
   /urix@0.1.0:
     resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
@@ -15372,6 +16502,15 @@
     resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
     dev: true
+
+  /url-parse@1.5.10:
+    resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
+    requiresBuild: true
+    dependencies:
+      querystringify: 2.2.0
+      requires-port: 1.0.0
+    dev: false
+    optional: true
 
   /url-toolkit@2.2.5:
     resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
@@ -15423,6 +16562,19 @@
     engines: {node: '>= 0.4.0'}
     dev: true
 
+  /uuid@3.4.0:
+    resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
+    deprecated: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
+    hasBin: true
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /uuid@8.3.2:
+    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+    hasBin: true
+    dev: false
+
   /v8-compile-cache-lib@3.0.1:
     resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
     dev: true
@@ -15440,6 +16592,17 @@
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
     dev: true
+
+  /verror@1.10.0:
+    resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
+    engines: {'0': node >=0.6.0}
+    requiresBuild: true
+    dependencies:
+      assert-plus: 1.0.0
+      core-util-is: 1.0.2
+      extsprintf: 1.3.0
+    dev: false
+    optional: true
 
   /vfile-location@2.0.6:
     resolution: {integrity: sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==}
@@ -15829,15 +16992,61 @@
       '@vue/shared': 3.5.11
       typescript: 4.8.4
 
+  /w3c-hr-time@1.0.2:
+    resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
+    deprecated: Use your platform's native performance.now() and performance.timeOrigin.
+    requiresBuild: true
+    dependencies:
+      browser-process-hrtime: 1.0.0
+    dev: false
+    optional: true
+
   /w3c-keyname@2.2.8:
     resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
     dev: false
+
+  /w3c-xmlserializer@1.1.2:
+    resolution: {integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==}
+    requiresBuild: true
+    dependencies:
+      domexception: 1.0.1
+      webidl-conversions: 4.0.2
+      xml-name-validator: 3.0.0
+    dev: false
+    optional: true
+
+  /w3c-xmlserializer@3.0.0:
+    resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      xml-name-validator: 4.0.0
+    dev: false
+    optional: true
 
   /wcwidth@1.0.1:
     resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
     dependencies:
       defaults: 1.0.4
     dev: true
+
+  /webidl-conversions@3.0.1:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+    requiresBuild: true
+    dev: false
+
+  /webidl-conversions@4.0.2:
+    resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /webidl-conversions@7.0.0:
+    resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dev: false
+    optional: true
 
   /webpack-sources@3.2.3:
     resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
@@ -15858,6 +17067,74 @@
     dependencies:
       sdp: 3.2.0
     dev: false
+
+  /whatwg-encoding@1.0.5:
+    resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
+    requiresBuild: true
+    dependencies:
+      iconv-lite: 0.4.24
+    dev: false
+    optional: true
+
+  /whatwg-encoding@2.0.0:
+    resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      iconv-lite: 0.6.3
+    dev: false
+    optional: true
+
+  /whatwg-mimetype@2.3.0:
+    resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /whatwg-mimetype@3.0.0:
+    resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dev: false
+    optional: true
+
+  /whatwg-url@10.0.0:
+    resolution: {integrity: sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      tr46: 3.0.0
+      webidl-conversions: 7.0.0
+    dev: false
+    optional: true
+
+  /whatwg-url@11.0.0:
+    resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
+    engines: {node: '>=12'}
+    requiresBuild: true
+    dependencies:
+      tr46: 3.0.0
+      webidl-conversions: 7.0.0
+    dev: false
+    optional: true
+
+  /whatwg-url@5.0.0:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+    requiresBuild: true
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+    dev: false
+
+  /whatwg-url@7.1.0:
+    resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
+    requiresBuild: true
+    dependencies:
+      lodash.sortby: 4.7.0
+      tr46: 1.0.1
+      webidl-conversions: 4.0.2
+    dev: false
+    optional: true
 
   /when-exit@2.1.3:
     resolution: {integrity: sha512-uVieSTccFIr/SFQdFWN/fFaQYmV37OKtuaGphMAzi4DmmUlrvRBJW5WSLkHyjNQY/ePJMz3LoiX9R3yy1Su6Hw==}
@@ -15926,6 +17203,14 @@
     hasBin: true
     dependencies:
       isexe: 2.0.0
+
+  /wide-align@1.1.5:
+    resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
+    requiresBuild: true
+    dependencies:
+      string-width: 4.2.3
+    dev: false
+    optional: true
 
   /widest-line@5.0.0:
     resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
@@ -16010,6 +17295,7 @@
 
   /wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+    requiresBuild: true
 
   /write-file-atomic@3.0.3:
     resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
@@ -16034,6 +17320,36 @@
     dependencies:
       mkdirp: 0.5.6
     dev: false
+
+  /ws@7.5.10:
+    resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+    engines: {node: '>=8.3.0'}
+    requiresBuild: true
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: false
+    optional: true
+
+  /ws@8.18.3:
+    resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+    engines: {node: '>=10.0.0'}
+    requiresBuild: true
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: false
+    optional: true
 
   /x-is-string@0.1.0:
     resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==}
@@ -16073,10 +17389,15 @@
       word: 0.3.0
     dev: false
 
+  /xml-name-validator@3.0.0:
+    resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /xml-name-validator@4.0.0:
     resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
     engines: {node: '>=12'}
-    dev: true
 
   /xml2js@0.6.2:
     resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
@@ -16091,6 +17412,12 @@
     engines: {node: '>=4.0'}
     dev: false
 
+  /xmlchars@2.2.0:
+    resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /xtend@4.0.2:
     resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
     engines: {node: '>=0.4'}
diff --git a/src/constants/apiEnum.ts b/src/constants/apiEnum.ts
index b91d90e..687a584 100644
--- a/src/constants/apiEnum.ts
+++ b/src/constants/apiEnum.ts
@@ -159,6 +159,18 @@
   Face = 30,
 }
 
+/** 涓汉鐢ㄦ埛瀹炲悕鐘舵�� */
+export enum EnumPersonalUserRealStatus {
+  /**鏈疄鍚� */
+  UnReal = 0,
+  /**鏍¢獙涓� */
+  Checking = 10,
+  /**瀹炲悕澶辫触 */
+  Fail = 99,
+  /**宸插疄鍚� */
+  Real = 100,
+}
+
 /** 瀹炲悕閫氶亾 */
 export enum EnumRealAccess {
   /**涓婁笂绛� */
diff --git a/src/constants/dic.ts b/src/constants/dic.ts
index 41e3e90..e7ee8f2 100644
--- a/src/constants/dic.ts
+++ b/src/constants/dic.ts
@@ -13,6 +13,8 @@
   IndustryCategory = '60',
   /**琛屾斂鍦板尯 */
   Area = '70',
+  /**琛屾斂鍦板尯 */
+  ElectronSignParam = '80',
 }
 
 export enum IdentityCodeEnum {
diff --git a/src/constants/electronSign.ts b/src/constants/electronSign.ts
index 7476a74..65a05cc 100644
--- a/src/constants/electronSign.ts
+++ b/src/constants/electronSign.ts
@@ -1,3 +1,5 @@
+import { EnumContractTemplateValueRecorder } from './apiEnum';
+
 export const EnumContractTemplateStatusText = {
   [EnumContractTemplateStatus.Wait]: '寰呭埗鐗�',
   [EnumContractTemplateStatus.Completed]: '宸插埗鐗�',
@@ -12,3 +14,20 @@
   [EnumElectronSignAccess.BestSign]: '涓婁笂绛�',
   [EnumElectronSignAccess.AlipaySign]: '鏀粯瀹濅俊浠荤',
 };
+
+export type TemplateEditDataItem = {
+  path: string;
+  width: number;
+  height: number;
+};
+
+export const EnumContractTemplateValueTypeText = {
+  [EnumContractTemplateValueType.Date]: '鏃ユ湡',
+  [EnumContractTemplateValueType.Sign]: '绛剧讲',
+  [EnumContractTemplateValueType.Text]: '鏂囨湰',
+};
+
+export const EnumContractTemplateValueRecorderText = {
+  [EnumContractTemplateValueRecorder.Creator]: '鍙戜欢浜�',
+  [EnumContractTemplateValueRecorder.Signer]: '绛剧讲浜�',
+};
diff --git a/src/fabric-editor/assets/filters/BlackWhite.png b/src/fabric-editor/assets/filters/BlackWhite.png
new file mode 100644
index 0000000..f6dc3ce
--- /dev/null
+++ b/src/fabric-editor/assets/filters/BlackWhite.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Brownie.png b/src/fabric-editor/assets/filters/Brownie.png
new file mode 100644
index 0000000..30c9ab2
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Brownie.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Invert.png b/src/fabric-editor/assets/filters/Invert.png
new file mode 100644
index 0000000..85b3367
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Invert.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Kodachrome.png b/src/fabric-editor/assets/filters/Kodachrome.png
new file mode 100644
index 0000000..0f82e3e
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Kodachrome.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Polaroid.png b/src/fabric-editor/assets/filters/Polaroid.png
new file mode 100644
index 0000000..605f870
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Polaroid.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Sepia.png b/src/fabric-editor/assets/filters/Sepia.png
new file mode 100644
index 0000000..a1e1071
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Sepia.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/Vintage.png b/src/fabric-editor/assets/filters/Vintage.png
new file mode 100644
index 0000000..1c2a877
--- /dev/null
+++ b/src/fabric-editor/assets/filters/Vintage.png
Binary files differ
diff --git a/src/fabric-editor/assets/filters/technicolor.png b/src/fabric-editor/assets/filters/technicolor.png
new file mode 100644
index 0000000..a31d4a9
--- /dev/null
+++ b/src/fabric-editor/assets/filters/technicolor.png
Binary files differ
diff --git "a/src/fabric-editor/assets/fonts/cn/\345\215\216\345\272\267\351\207\221\345\210\232\351\273\221.ttf" "b/src/fabric-editor/assets/fonts/cn/\345\215\216\345\272\267\351\207\221\345\210\232\351\273\221.ttf"
new file mode 100644
index 0000000..c0f598b
--- /dev/null
+++ "b/src/fabric-editor/assets/fonts/cn/\345\215\216\345\272\267\351\207\221\345\210\232\351\273\221.ttf"
Binary files differ
diff --git "a/src/fabric-editor/assets/fonts/cn/\346\261\211\344\275\223.ttf" "b/src/fabric-editor/assets/fonts/cn/\346\261\211\344\275\223.ttf"
new file mode 100644
index 0000000..3729151
--- /dev/null
+++ "b/src/fabric-editor/assets/fonts/cn/\346\261\211\344\275\223.ttf"
Binary files differ
diff --git a/src/fabric-editor/assets/fonts/font.css b/src/fabric-editor/assets/fonts/font.css
new file mode 100644
index 0000000..1b396a0
--- /dev/null
+++ b/src/fabric-editor/assets/fonts/font.css
@@ -0,0 +1,9 @@
+@font-face {
+  font-family: '姹変綋';
+  src: url('./cn/姹変綋.ttf');
+}
+
+@font-face {
+  font-family: '鍗庡悍閲戝垰榛�';
+  src: url('./cn/鍗庡悍閲戝垰榛�.ttf');
+}
diff --git a/src/fabric-editor/assets/fonts/font.js b/src/fabric-editor/assets/fonts/font.js
new file mode 100644
index 0000000..8c11bbe
--- /dev/null
+++ b/src/fabric-editor/assets/fonts/font.js
@@ -0,0 +1,22 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-05 22:54:14
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2022-09-05 22:59:30
+ * @Description: 瀛椾綋鏂囦欢鍒楄〃
+ */
+
+const cnList = [
+  {
+    name: '姹変綋',
+    fontFamily: '姹変綋',
+  },
+  {
+    name: '鍗庡悍閲戝垰榛�',
+    fontFamily: '鍗庡悍閲戝垰榛�',
+  },
+];
+
+const enList = [];
+
+export default [...cnList, ...enList];
diff --git a/src/fabric-editor/assets/icon/align/averageX.svg b/src/fabric-editor/assets/icon/align/averageX.svg
new file mode 100644
index 0000000..4c4a41f
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/averageX.svg
@@ -0,0 +1,6 @@
+<svg t="1650442800956"  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="1910" >
+          <path
+            d="M96 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32A32 32 0 0 1 96 0z m832 0a32 32 0 0 1 32 32v960a32 32 0 0 1-64 0V32a32 32 0 0 1 32-32zM384 800v-576a32 32 0 0 1 32-32h192a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32h-192a32 32 0 0 1-32-32z"
+            fill="#515151" p-id="1911"></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/averageY.svg b/src/fabric-editor/assets/icon/align/averageY.svg
new file mode 100644
index 0000000..510c3d0
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/averageY.svg
@@ -0,0 +1,6 @@
+ <svg t="1650442784286"  viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="1712" >
+          <path
+            d="M1170.285714 36.571429a36.571429 36.571429 0 0 1-36.571428 36.571428H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571429z m0 950.857142a36.571429 36.571429 0 0 1-36.571428 36.571429H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571428zM256 365.714286h658.285714a36.571429 36.571429 0 0 1 36.571429 36.571428v219.428572a36.571429 36.571429 0 0 1-36.571429 36.571428h-658.285714a36.571429 36.571429 0 0 1-36.571429-36.571428v-219.428572a36.571429 36.571429 0 0 1 36.571429-36.571428z"
+            fill="#515151" p-id="1713"></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/bottom.svg b/src/fabric-editor/assets/icon/align/bottom.svg
new file mode 100644
index 0000000..318062b
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/bottom.svg
@@ -0,0 +1,6 @@
+ <svg t="1650442674784" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="920" >
+          <path
+            d="M1170.285714 987.428571a36.571429 36.571429 0 0 0-36.571428-36.571428H36.571429a36.571429 36.571429 0 0 0 0 73.142857h1097.142857a36.571429 36.571429 0 0 0 36.571428-36.571429z m-219.428571-146.285714v-512a36.571429 36.571429 0 0 0-36.571429-36.571428h-219.428571a36.571429 36.571429 0 0 0-36.571429 36.571428v512a36.571429 36.571429 0 0 0 36.571429 36.571429h219.428571a36.571429 36.571429 0 0 0 36.571429-36.571429z m-438.857143 0V36.571429a36.571429 36.571429 0 0 0-36.571429-36.571429h-219.428571a36.571429 36.571429 0 0 0-36.571429 36.571429v804.571428a36.571429 36.571429 0 0 0 36.571429 36.571429h219.428571a36.571429 36.571429 0 0 0 36.571429-36.571429z"
+            fill="#666666" p-id="921"></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/centerX.svg b/src/fabric-editor/assets/icon/align/centerX.svg
new file mode 100644
index 0000000..aa28edb
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/centerX.svg
@@ -0,0 +1,14 @@
+ <svg
+          t="1650442754876"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="1514"
+          
+        >
+          <path
+            d="M477.312 576V448H266.688a32 32 0 0 1-32-32v-192a32 32 0 0 1 32-32h210.624V34.688a34.688 34.688 0 0 1 69.376 0V192h210.624a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H546.688v128H896a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H546.688v157.312a34.688 34.688 0 1 1-69.376 0V832H128a32 32 0 0 1-32-32v-192A32 32 0 0 1 128 576h349.312z"
+            fill="#666666"
+            p-id="1515"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/centerY.svg b/src/fabric-editor/assets/icon/align/centerY.svg
new file mode 100644
index 0000000..8be0c1e
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/centerY.svg
@@ -0,0 +1,6 @@
+ <svg t="1650442732396"  viewBox="0 0 1243 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="1316" >
+          <path
+            d="M548.571429 472.356571h146.285714V231.643429a36.571429 36.571429 0 0 1 36.571428-36.571429h219.428572a36.571429 36.571429 0 0 1 36.571428 36.571429v240.713142h179.785143a39.643429 39.643429 0 0 1 0 79.286858H987.428571v240.713142a36.571429 36.571429 0 0 1-36.571428 36.571429h-219.428572a36.571429 36.571429 0 0 1-36.571428-36.571429V551.643429h-146.285714V950.857143a36.571429 36.571429 0 0 1-36.571429 36.571428H292.571429a36.571429 36.571429 0 0 1-36.571429-36.571428V551.643429H76.214857a39.643429 39.643429 0 1 1 0-79.286858H256V73.142857A36.571429 36.571429 0 0 1 292.571429 36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571428v399.213714z"
+            fill="#666666" p-id="1317"></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/left.svg b/src/fabric-editor/assets/icon/align/left.svg
new file mode 100644
index 0000000..1d8f679
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/left.svg
@@ -0,0 +1,10 @@
+ <svg
+    t="1650442284704"
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="2345"
+   
+>
+    <path d="M80 24h64v976H80zM198 227h448v190H198zM198 607h746v190H198z" p-id="2346"></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/right.svg b/src/fabric-editor/assets/icon/align/right.svg
new file mode 100644
index 0000000..b25d3dc
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/right.svg
@@ -0,0 +1,4 @@
+<svg t="1650442299564"  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="2543" >
+          <path d="M944 1000h-64V24h64zM826 417H378V227h448zM826 797H80V607h746z" p-id="2544"></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/align/top.svg b/src/fabric-editor/assets/icon/align/top.svg
new file mode 100644
index 0000000..54e3d0c
--- /dev/null
+++ b/src/fabric-editor/assets/icon/align/top.svg
@@ -0,0 +1,6 @@
+  <svg t="1650442692910"  viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+          p-id="1118" >
+          <path
+            d="M1170.285714 36.571429a36.571429 36.571429 0 0 1-36.571428 36.571428H36.571429a36.571429 36.571429 0 0 1 0-73.142857h1097.142857a36.571429 36.571429 0 0 1 36.571428 36.571429z m-219.428571 146.285714v512a36.571429 36.571429 0 0 1-36.571429 36.571428h-219.428571a36.571429 36.571429 0 0 1-36.571429-36.571428v-512a36.571429 36.571429 0 0 1 36.571429-36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571429z m-438.857143 0v804.571428a36.571429 36.571429 0 0 1-36.571429 36.571429h-219.428571a36.571429 36.571429 0 0 1-36.571429-36.571429v-804.571428a36.571429 36.571429 0 0 1 36.571429-36.571429h219.428571a36.571429 36.571429 0 0 1 36.571429 36.571429z"
+            fill="#666666" p-id="1119"></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/fontStyle.svg b/src/fabric-editor/assets/icon/attribute/fontStyle.svg
new file mode 100644
index 0000000..1adae71
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/fontStyle.svg
@@ -0,0 +1,5 @@
+ <svg viewBox="0 0 1024 1024" >
+                <path
+                  d="M832 96v64a32 32 0 0 1-32 32h-125.52l-160 640H608a32 32 0 0 1 32 32v64a32 32 0 0 1-32 32H224a32 32 0 0 1-32-32v-64a32 32 0 0 1 32-32h125.52l160-640H416a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32h384a32 32 0 0 1 32 32z"
+                ></path>
+              </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/fontWeight.svg b/src/fabric-editor/assets/icon/attribute/fontWeight.svg
new file mode 100644
index 0000000..ce3c676
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/fontWeight.svg
@@ -0,0 +1,5 @@
+ <svg viewBox="0 0 1024 1024" >
+        <path
+            d="M793.99865 476a244 244 0 0 0 54-130.42C862.75865 192.98 743.01865 64 593.85865 64H195.01865a32 32 0 0 0-32 32v96a32 32 0 0 0 32 32h63.74v576H195.01865a32 32 0 0 0-32 32v96a32 32 0 0 0 32 32h418.64c141.6 0 268.28-103.5 282-244.8 9.48-96.9-32.78-184.12-101.66-239.2zM418.33865 224h175.52a96 96 0 0 1 0 192h-175.52z m175.52 576h-175.52V576h175.52a112 112 0 0 1 0 224z"
+        ></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/linethrough.svg b/src/fabric-editor/assets/icon/attribute/linethrough.svg
new file mode 100644
index 0000000..432d378
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/linethrough.svg
@@ -0,0 +1,5 @@
+<svg viewBox="0 0 1024 1024" >
+                <path
+                  d="M893.088 501.792H125.344a32 32 0 0 0 0 64h767.744a32 32 0 0 0 0-64zM448 448h112V208h288V96H160v112h288zM448 640h112v288H448z"
+                ></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/textAlignCenter.svg b/src/fabric-editor/assets/icon/attribute/textAlignCenter.svg
new file mode 100644
index 0000000..c712bb3
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/textAlignCenter.svg
@@ -0,0 +1 @@
+<svg t="1650441512015" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3704" width="18" height="18"><path d="M313.6 198.4h398.933333c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-398.933333c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 10.666667-8.533333 19.2-8.533333z m-115.2 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m115.2 170.666666h398.933333c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-398.933333c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 10.666667-8.533333 19.2-8.533334z m-115.2 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z" p-id="3705"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/textAlignJustitfy.svg b/src/fabric-editor/assets/icon/attribute/textAlignJustitfy.svg
new file mode 100644
index 0000000..5975d2b
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/textAlignJustitfy.svg
@@ -0,0 +1 @@
+<svg width="18.000000" height="18.000000" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path id="path" d="M3.48 3.48L14.48 3.48C14.63 3.48 14.75 3.52 14.82 3.63C14.93 3.75 14.97 3.86 14.97 3.97L14.97 4.98C14.97 5.13 14.93 5.25 14.82 5.32C14.71 5.43 14.6 5.47 14.48 5.47L3.48 5.47C3.33 5.47 3.22 5.43 3.15 5.32C3.03 5.21 3 5.1 3 4.98L3 3.97C3 3.82 3.03 3.71 3.15 3.63C3.22 3.56 3.37 3.48 3.48 3.48ZM3.48 6.48L14.5 6.48C14.65 6.48 14.76 6.52 14.83 6.63C14.95 6.75 14.98 6.86 14.98 6.97L14.98 7.98C14.98 8.13 14.95 8.25 14.83 8.32C14.72 8.43 14.61 8.47 14.5 8.47L3.48 8.47C3.33 8.47 3.22 8.43 3.15 8.32C3.03 8.21 3 8.1 3 7.98L3 6.97C3 6.82 3.03 6.71 3.15 6.63C3.22 6.56 3.37 6.48 3.48 6.48ZM3.48 9.48L14.47 9.48C14.62 9.48 14.73 9.52 14.81 9.63C14.92 9.75 14.96 9.86 14.96 9.97L14.96 10.98C14.96 11.13 14.92 11.25 14.81 11.32C14.7 11.43 14.58 11.47 14.47 11.47L3.48 11.47C3.33 11.47 3.22 11.43 3.15 11.32C3.03 11.21 3 11.1 3 10.98L3 9.97C3 9.82 3.03 9.71 3.15 9.63C3.22 9.56 3.37 9.48 3.48 9.48ZM3.48 12.48L14.47 12.48C14.62 12.48 14.73 12.52 14.81 12.63C14.92 12.75 14.96 12.86 14.96 12.97L14.96 13.98C14.96 14.13 14.92 14.25 14.81 14.32C14.7 14.43 14.58 14.47 14.47 14.47L3.48 14.47C3.33 14.47 3.22 14.43 3.15 14.32C3.03 14.21 3 14.1 3 13.98L3 12.97C3 12.82 3.03 12.71 3.15 12.63C3.22 12.56 3.37 12.48 3.48 12.48Z" fill-rule="nonzero"/></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/textAlignLeft.svg b/src/fabric-editor/assets/icon/attribute/textAlignLeft.svg
new file mode 100644
index 0000000..4f92a76
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/textAlignLeft.svg
@@ -0,0 +1 @@
+<svg t="1650441458823"  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3554" width="18" height="18"><path d="M198.4 198.4h341.333333c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-341.333333c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m0 170.666667h569.6c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-569.6c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m0 170.666666h454.4c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-454.4c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533334z m0 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z" p-id="3555"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/textAlignRight.svg b/src/fabric-editor/assets/icon/attribute/textAlignRight.svg
new file mode 100644
index 0000000..393b3db
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/textAlignRight.svg
@@ -0,0 +1 @@
+<svg t="1650441519862" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3854" width="18" height="18"><path d="M454.4 283.733333v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h341.333334c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-341.333334c-8.533333 0-14.933333-2.133333-19.2-8.533334-4.266667-4.266667-8.533333-10.666667-8.533333-19.2z m-226.133333 170.666667v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h569.6c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333H256c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-4.266667-8.533333-10.666667-8.533333-19.2z m113.066666 170.666667v-57.6c0-8.533333 2.133333-14.933333 8.533334-19.2 6.4-6.4 12.8-8.533333 19.2-8.533334h454.4c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-454.4c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-4.266667-8.533333-10.666667-8.533334-19.2z m-170.666666 170.666666v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-4.266667-8.533333-10.666667-8.533333-19.2z" p-id="3855"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/attribute/underline.svg b/src/fabric-editor/assets/icon/attribute/underline.svg
new file mode 100644
index 0000000..0624dd7
--- /dev/null
+++ b/src/fabric-editor/assets/icon/attribute/underline.svg
@@ -0,0 +1,6 @@
+ <svg viewBox="0 0 1024 1024" >
+                <path
+                  d="M703.232 67.008h127.488v413.248c0 158.016-142.656 286.016-318.72 286.016-176 0-318.72-128-318.72-286.016V67.008h127.488v413.248c0 39.872 18.176 78.144 51.136 107.776 36.8 32.96 86.528 51.072 140.096 51.072s103.36-18.112 140.032-51.136c33.024-29.632 51.2-67.968 51.2-107.776V67.008zM193.28 871.616h637.44v85.376H193.28v-85.376z"
+                
+                ></path>
+              </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/barcode/center.svg b/src/fabric-editor/assets/icon/barcode/center.svg
new file mode 100644
index 0000000..c712bb3
--- /dev/null
+++ b/src/fabric-editor/assets/icon/barcode/center.svg
@@ -0,0 +1 @@
+<svg t="1650441512015" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3704" width="18" height="18"><path d="M313.6 198.4h398.933333c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-398.933333c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 10.666667-8.533333 19.2-8.533333z m-115.2 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m115.2 170.666666h398.933333c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-398.933333c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 10.666667-8.533333 19.2-8.533334z m-115.2 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z" p-id="3705"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/barcode/left.svg b/src/fabric-editor/assets/icon/barcode/left.svg
new file mode 100644
index 0000000..b6248ea
--- /dev/null
+++ b/src/fabric-editor/assets/icon/barcode/left.svg
@@ -0,0 +1 @@
+<svg t="1650441458823" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3554" width="18" height="18"><path d="M198.4 198.4h341.333333c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533334 19.2v57.6c0 8.533333-2.133333 14.933333-8.533334 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-341.333333c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m0 170.666667h569.6c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-569.6c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z m0 170.666666h454.4c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-454.4c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533334z m0 170.666667h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-6.4-8.533333-12.8-8.533333-19.2v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 4.266667-4.266667 12.8-8.533333 19.2-8.533333z" p-id="3555"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/barcode/right.svg b/src/fabric-editor/assets/icon/barcode/right.svg
new file mode 100644
index 0000000..393b3db
--- /dev/null
+++ b/src/fabric-editor/assets/icon/barcode/right.svg
@@ -0,0 +1 @@
+<svg t="1650441519862" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3854" width="18" height="18"><path d="M454.4 283.733333v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h341.333334c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-341.333334c-8.533333 0-14.933333-2.133333-19.2-8.533334-4.266667-4.266667-8.533333-10.666667-8.533333-19.2z m-226.133333 170.666667v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h569.6c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333H256c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-4.266667-8.533333-10.666667-8.533333-19.2z m113.066666 170.666667v-57.6c0-8.533333 2.133333-14.933333 8.533334-19.2 6.4-6.4 12.8-8.533333 19.2-8.533334h454.4c8.533333 0 14.933333 2.133333 19.2 8.533334 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533333h-454.4c-8.533333 0-14.933333-2.133333-19.2-8.533333-6.4-4.266667-8.533333-10.666667-8.533334-19.2z m-170.666666 170.666666v-57.6c0-8.533333 2.133333-14.933333 8.533333-19.2 6.4-6.4 12.8-8.533333 19.2-8.533333h625.066667c8.533333 0 14.933333 2.133333 19.2 8.533333 6.4 6.4 8.533333 12.8 8.533333 19.2v57.6c0 8.533333-2.133333 14.933333-8.533333 19.2-6.4 6.4-12.8 8.533333-19.2 8.533334h-625.066667c-8.533333 0-14.933333-2.133333-19.2-8.533334-6.4-4.266667-8.533333-10.666667-8.533333-19.2z" p-id="3855"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/bottom.svg b/src/fabric-editor/assets/icon/bottom.svg
new file mode 100644
index 0000000..3204133
--- /dev/null
+++ b/src/fabric-editor/assets/icon/bottom.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113358706" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5436" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M874.666667 810.666667a21.333333 21.333333 0 0 0-21.333334-21.333334H213.333333a21.333333 21.333333 0 1 0 0 42.666667h640a21.333333 21.333333 0 0 0 21.333334-21.333333z m-384-576a42.666667 42.666667 0 0 0-42.666667-42.666667h-85.333333a42.666667 42.666667 0 0 0-42.666667 42.666667v426.666666a42.666667 42.666667 0 0 0 42.666667 42.666667h85.333333a42.666667 42.666667 0 0 0 42.666667-42.666667v-426.666666z m256 213.333333a42.666667 42.666667 0 0 0-42.666667-42.666667h-85.333333a42.666667 42.666667 0 0 0-42.666667 42.666667v213.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h85.333333a42.666667 42.666667 0 0 0 42.666667-42.666667v-213.333333z" fill="#666666" p-id="5437"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/centerAlign/center.svg b/src/fabric-editor/assets/icon/centerAlign/center.svg
new file mode 100644
index 0000000..e32b78f
--- /dev/null
+++ b/src/fabric-editor/assets/icon/centerAlign/center.svg
@@ -0,0 +1,18 @@
+ <svg
+          t="1650852784867"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="2351"
+        >
+          <path
+            d="M544 480V64h-64v416H64v64h416v416h64V544h416v-64z"
+            fill="#727272"
+            p-id="2352"
+          ></path>
+          <path
+            d="M123.7 241.1h119.5v64H123.7zM302.9 241.1h119.5v64H302.9zM601.6 241.1h119.5v64H601.6zM780.8 241.1h119.5v64H780.8zM123.7 718.9h119.5v64H123.7zM302.9 718.9h119.5v64H302.9zM601.6 718.9h119.5v64H601.6zM780.8 718.9h119.5v64H780.8z"
+            fill="#B2B2B2"
+            p-id="2353"
+          ></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/centerAlign/centerX.svg b/src/fabric-editor/assets/icon/centerAlign/centerX.svg
new file mode 100644
index 0000000..1d0b5a5
--- /dev/null
+++ b/src/fabric-editor/assets/icon/centerAlign/centerX.svg
@@ -0,0 +1,12 @@
+ <svg
+          t="1650442559691"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="3787"
+        >
+          <path
+            d="M885 607H544V417h192V227H544V24h-64v203H288v190h192v190H139v190h341v203h64V797h341z"
+            p-id="3788"
+          ></path>
+</svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/centerAlign/centerY.svg b/src/fabric-editor/assets/icon/centerAlign/centerY.svg
new file mode 100644
index 0000000..b50b841
--- /dev/null
+++ b/src/fabric-editor/assets/icon/centerAlign/centerY.svg
@@ -0,0 +1,12 @@
+   <svg
+          t="1650442510967"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="3412"
+        >
+          <path
+            d="M859.9 474H164.1c-4.5 0-8.1 3.6-8.1 8v60c0 4.4 3.6 8 8.1 8h695.8c4.5 0 8.1-3.6 8.1-8v-60c0-4.4-3.6-8-8.1-8z m-353.6-74.7c2.9 3.7 8.5 3.7 11.3 0l100.8-127.5c3.7-4.7 0.4-11.7-5.7-11.7H550V104c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v156h-62.8c-6 0-9.4 7-5.7 11.7l100.8 127.6z m11.4 225.4c-2.9-3.7-8.5-3.7-11.3 0L405.6 752.3c-3.7 4.7-0.4 11.7 5.7 11.7H474v156c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V764h62.8c6 0 9.4-7 5.7-11.7L517.7 624.7z"
+            p-id="3413"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/centerx.svg b/src/fabric-editor/assets/icon/centerx.svg
new file mode 100644
index 0000000..c8be333
--- /dev/null
+++ b/src/fabric-editor/assets/icon/centerx.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113328975" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5260" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 170.666667a21.333333 21.333333 0 0 1 21.333333 21.333333V298.666667h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666666v85.333334a42.666667 42.666667 0 0 1-42.666666 42.666666h-85.333334v85.333334H725.333333a42.666667 42.666667 0 0 1 42.666667 42.666666v85.333334a42.666667 42.666667 0 0 1-42.666667 42.666666h-192v106.666667a21.333333 21.333333 0 1 1-42.666666 0V725.333333H298.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-85.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h192v-85.333334h-85.333334a42.666667 42.666667 0 0 1-42.666666-42.666666V341.333333a42.666667 42.666667 0 0 1 42.666666-42.666666h85.333334V192A21.333333 21.333333 0 0 1 512 170.666667z" fill="#666666" p-id="5261"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/centery.svg b/src/fabric-editor/assets/icon/centery.svg
new file mode 100644
index 0000000..32872ee
--- /dev/null
+++ b/src/fabric-editor/assets/icon/centery.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113314954" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4732" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 512a21.333333 21.333333 0 0 1-21.333333 21.333333H725.333333v85.333334a42.666667 42.666667 0 0 1-42.666666 42.666666h-85.333334a42.666667 42.666667 0 0 1-42.666666-42.666666v-85.333334h-85.333334V725.333333a42.666667 42.666667 0 0 1-42.666666 42.666667H341.333333a42.666667 42.666667 0 0 1-42.666666-42.666667v-192H192a21.333333 21.333333 0 1 1 0-42.666666H298.666667V298.666667a42.666667 42.666667 0 0 1 42.666666-42.666667h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v192h85.333334v-85.333334a42.666667 42.666667 0 0 1 42.666666-42.666666h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666666v85.333334h106.666667a21.333333 21.333333 0 0 1 21.333333 21.333333z" fill="#666666" p-id="4733"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/fileType.png b/src/fabric-editor/assets/icon/fileType.png
new file mode 100644
index 0000000..a5693ec
--- /dev/null
+++ b/src/fabric-editor/assets/icon/fileType.png
Binary files differ
diff --git a/src/fabric-editor/assets/icon/flip/x.svg b/src/fabric-editor/assets/icon/flip/x.svg
new file mode 100644
index 0000000..ff35397
--- /dev/null
+++ b/src/fabric-editor/assets/icon/flip/x.svg
@@ -0,0 +1,22 @@
+ <svg
+          t="1650443094178"
+         
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="1549"
+        
+        >
+          <path
+            d="M252.76928 299.904l146.2784 0 0 472.42752-146.2784 0 0-472.42752Z"
+            p-id="1550"
+          ></path>
+          <path
+            d="M477.48096 85.34528l70.87104 0 0 885.80608-70.87104 0 0-885.80608Z"
+            p-id="1551"
+          ></path>
+          <path
+            d="M629.80096 284.8l31.0016 0 0 502.88128-31.0016 0L629.80096 284.8zM776.42752 284.8l31.0016 0 0 502.88128-31.0016 0L776.42752 284.8zM657.09056 315.8016l0-31.0016 123.04896 0 0 31.0016L657.09056 315.8016zM657.27488 787.64544l0-31.0016 123.04896 0 0 31.0016L657.27488 787.64544z"
+            p-id="1552"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/flip/y.svg b/src/fabric-editor/assets/icon/flip/y.svg
new file mode 100644
index 0000000..70f1f21
--- /dev/null
+++ b/src/fabric-editor/assets/icon/flip/y.svg
@@ -0,0 +1,21 @@
+ <svg
+          t="1650443104385"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="1749"
+         
+        >
+          <path
+            d="M286.01856 250.91584l472.4224 0 0 146.2784-472.4224 0 0-146.2784Z"
+            p-id="1750"
+          ></path>
+          <path
+            d="M87.19872 475.62752l885.80096 0 0 70.87104-885.80096 0 0-70.87104Z"
+            p-id="1751"
+          ></path>
+          <path
+            d="M773.55008 627.94752l0 31.0016L270.6688 658.94912l0-31.0016L773.55008 627.94752zM773.55008 774.5792l0 31.0016L270.6688 805.5808l0-31.0016L773.55008 774.5792zM742.54848 655.24224l31.0016 0 0 123.04896-31.0016 0L742.54848 655.24224zM270.70464 655.42144l31.0016 0 0 123.04896-31.0016 0L270.70464 655.42144z"
+            p-id="1752"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/group/group.svg b/src/fabric-editor/assets/icon/group/group.svg
new file mode 100644
index 0000000..a2f1a92
--- /dev/null
+++ b/src/fabric-editor/assets/icon/group/group.svg
@@ -0,0 +1,13 @@
+ <svg
+        t="1650848913991"
+      
+        viewBox="0 0 1024 1024"
+        version="1.1"
+        xmlns="http://www.w3.org/2000/svg"
+        p-id="17131"
+      >
+        <path
+          d="M341.333333 341.333333 341.333333 512 554.666667 512 554.666667 341.333333 341.333333 341.333333M42.666667 42.666667 213.333333 42.666667 213.333333 85.333333 810.666667 85.333333 810.666667 42.666667 981.333333 42.666667 981.333333 213.333333 938.666667 213.333333 938.666667 810.666667 981.333333 810.666667 981.333333 981.333333 810.666667 981.333333 810.666667 938.666667 213.333333 938.666667 213.333333 981.333333 42.666667 981.333333 42.666667 810.666667 85.333333 810.666667 85.333333 213.333333 42.666667 213.333333 42.666667 42.666667M213.333333 810.666667 213.333333 853.333333 810.666667 853.333333 810.666667 810.666667 853.333333 810.666667 853.333333 213.333333 810.666667 213.333333 810.666667 170.666667 213.333333 170.666667 213.333333 213.333333 170.666667 213.333333 170.666667 810.666667 213.333333 810.666667M256 256 640 256 640 426.666667 768 426.666667 768 768 341.333333 768 341.333333 597.333333 256 597.333333 256 256M640 597.333333 426.666667 597.333333 426.666667 682.666667 682.666667 682.666667 682.666667 512 640 512 640 597.333333Z"
+          p-id="17132"
+        ></path>
+      </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/group/unGroup.svg b/src/fabric-editor/assets/icon/group/unGroup.svg
new file mode 100644
index 0000000..ad7bad7
--- /dev/null
+++ b/src/fabric-editor/assets/icon/group/unGroup.svg
@@ -0,0 +1,13 @@
+  <svg
+        t="1650848938557"
+        viewBox="0 0 1024 1024"
+        version="1.1"
+        xmlns="http://www.w3.org/2000/svg"
+        p-id="17281"
+       
+      >
+        <path
+          d="M85.333333 85.333333 256 85.333333 256 128 554.666667 128 554.666667 85.333333 725.333333 85.333333 725.333333 256 682.666667 256 682.666667 384 768 384 768 341.333333 938.666667 341.333333 938.666667 512 896 512 896 768 938.666667 768 938.666667 938.666667 768 938.666667 768 896 512 896 512 938.666667 341.333333 938.666667 341.333333 768 384 768 384 682.666667 256 682.666667 256 725.333333 85.333333 725.333333 85.333333 554.666667 128 554.666667 128 256 85.333333 256 85.333333 85.333333M768 512 768 469.333333 682.666667 469.333333 682.666667 554.666667 725.333333 554.666667 725.333333 725.333333 554.666667 725.333333 554.666667 682.666667 469.333333 682.666667 469.333333 768 512 768 512 810.666667 768 810.666667 768 768 810.666667 768 810.666667 512 768 512M554.666667 256 554.666667 213.333333 256 213.333333 256 256 213.333333 256 213.333333 554.666667 256 554.666667 256 597.333333 384 597.333333 384 512 341.333333 512 341.333333 341.333333 512 341.333333 512 384 597.333333 384 597.333333 256 554.666667 256M512 512 469.333333 512 469.333333 597.333333 554.666667 597.333333 554.666667 554.666667 597.333333 554.666667 597.333333 469.333333 512 469.333333 512 512Z"
+          p-id="17282"
+        ></path>
+      </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/circle.svg b/src/fabric-editor/assets/icon/layer/circle.svg
new file mode 100644
index 0000000..8f165a2
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/circle.svg
@@ -0,0 +1 @@
+<svg t="1650855860236" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19440" width="16" height="16"><path d="M512 928C282.624 928 96 741.376 96 512S282.624 96 512 96s416 186.624 416 416-186.624 416-416 416z m0-768C317.92 160 160 317.92 160 512s157.92 352 352 352 352-157.92 352-352S706.08 160 512 160z" p-id="19441"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/default.svg b/src/fabric-editor/assets/icon/layer/default.svg
new file mode 100644
index 0000000..c470303
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/default.svg
@@ -0,0 +1 @@
+<svg t="1650855578257" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17630" width="16" height="16"><path d="M620.606061 0a62.060606 62.060606 0 0 1 62.060606 62.060606v188.943515C874.945939 273.997576 1024 437.651394 1024 636.121212c0 214.217697-173.661091 387.878788-387.878788 387.878788-198.469818 0-362.123636-149.054061-385.117091-341.333333H62.060606a62.060606 62.060606 0 0 1-62.060606-62.060606V62.060606a62.060606 62.060606 0 0 1 62.060606-62.060606h558.545455z m62.060606 297.937455V620.606061a62.060606 62.060606 0 0 1-62.060606 62.060606H297.937455C320.636121 849.159758 463.39103 977.454545 636.121212 977.454545c188.509091 0 341.333333-152.824242 341.333333-341.333333 0-172.730182-128.294788-315.485091-294.787878-338.183757zM620.606061 46.545455H62.060606a15.515152 15.515152 0 0 0-15.406545 13.699878L46.545455 62.060606v558.545455a15.515152 15.515152 0 0 0 13.699878 15.406545L62.060606 636.121212h186.181818c0-214.217697 173.661091-387.878788 387.878788-387.878788V62.060606a15.515152 15.515152 0 0 0-13.699879-15.406545L620.606061 46.545455z m15.515151 248.242424c-188.509091 0-341.333333 152.824242-341.333333 341.333333h325.818182a15.515152 15.515152 0 0 0 15.406545-13.699879L636.121212 620.606061V294.787879z" p-id="17631"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/down.svg b/src/fabric-editor/assets/icon/layer/down.svg
new file mode 100644
index 0000000..54a1cc6
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/down.svg
@@ -0,0 +1 @@
+<svg t="1650442229022" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1997" width="12" height="12"><path d="M876.2 589.7L536.7 929.1c-6.6 6.6-15.5 10.3-24.7 10.3-9.3 0-18.2-3.7-24.7-10.3L147.8 589.7c-13.7-13.7-13.7-35.8 0-49.5 13.7-13.7 35.8-13.7 49.5 0L477 819.9V119.6c0-19.3 15.7-35 35-35s35 15.7 35 35v700.2l279.7-279.7c6.8-6.8 15.8-10.3 24.7-10.3s17.9 3.4 24.7 10.3c13.7 13.8 13.7 35.9 0.1 49.6z" p-id="1998" ></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/downTop.svg b/src/fabric-editor/assets/icon/layer/downTop.svg
new file mode 100644
index 0000000..df98aa6
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/downTop.svg
@@ -0,0 +1 @@
+<svg t="1650442146918" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2045" width="11" height="11"><path d="M548.352 804.352a58.88 58.88 0 0 1-16.896 10.752 51.2 51.2 0 0 1-38.912 0 58.88 58.88 0 0 1-16.896-10.752l-256-256a51.2 51.2 0 0 1 72.704-72.704L460.8 644.608V51.2a51.2 51.2 0 0 1 102.4 0v593.408l168.448-168.96a51.2 51.2 0 0 1 72.704 72.704zM972.8 1024H51.2a51.2 51.2 0 0 1 0-102.4h921.6a51.2 51.2 0 0 1 0 102.4z" p-id="2046"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/group.svg b/src/fabric-editor/assets/icon/layer/group.svg
new file mode 100644
index 0000000..9b62d46
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/group.svg
@@ -0,0 +1 @@
+<svg t="1650855307397"  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2503" width="16" height="16"><path d="M839.036 130.458h-654.072c-30.102 0-54.506 24.404-54.506 54.506v654.072c0 30.102 24.404 54.506 54.506 54.506h654.072c30.102 0 54.506-24.404 54.506-54.506v-654.072c0-30.102-24.404-54.506-54.506-54.506zM839.036 811.786c0 15.050-12.196 27.249-27.249 27.249h-598.721c-15.050 0-27.249-12.196-27.249-27.249v-598.721c0-15.050 12.196-27.249 27.249-27.249h598.721c15.049 0 27.249 12.196 27.249 27.249v598.721zM730.028 421.639h-127.324v-126.817c0-30.091-24.402-54.499-54.501-54.499h-252.755c-30.098 0-54.501 24.401-54.501 54.499v253.89c0 30.091 24.402 54.499 54.501 54.499h127.324v126.817c0 30.091 24.402 54.499 54.501 54.499h252.755c30.098 0 54.501-24.401 54.501-54.499v-253.89c0-30.091-24.402-54.499-54.501-54.499zM323.36 548.137c-15.050 0-27.251-12.207-27.251-27.26v-197.694c0-15.055 12.201-27.26 27.251-27.26h196.928c15.051 0 27.251 12.207 27.251 27.26v98.458h-70.267c-30.098 0-54.501 24.401-54.501 54.499v71.998h-99.411zM547.539 477.24v43.638c0 15.055-12.202 27.26-27.251 27.26h-42.353v-43.638c0-15.055 12.202-27.26 27.251-27.26h42.353zM729.365 702.193c0 15.055-12.201 27.26-27.251 27.26h-196.928c-15.050 0-27.251-12.207-27.251-27.26v-98.981h70.267c30.098 0 54.501-24.401 54.501-54.499v-71.474h99.411c15.050 0 27.251 12.207 27.251 27.26v197.693z" p-id="2504" ></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/iText.svg b/src/fabric-editor/assets/icon/layer/iText.svg
new file mode 100644
index 0000000..68f5741
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/iText.svg
@@ -0,0 +1 @@
+<svg t="1650875455324" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5401" width="16" height="16"><path d="M213.333333 209.92v128h85.333334v-42.666667h170.666666v433.493334H384.853333v85.333333h256v-85.333333H554.666667V295.253333h170.666666v42.666667h85.333334v-128H213.333333z" p-id="5402"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/image.svg b/src/fabric-editor/assets/icon/layer/image.svg
new file mode 100644
index 0000000..e9b3533
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/image.svg
@@ -0,0 +1 @@
+<svg t="1650855321307" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2701" width="16" height="16"><path d="M813.752 223.168H209.584a25.168 25.168 0 0 0-25.168 25.176v528.648a25.16 25.16 0 0 0 25.168 25.168h604.168a25.152 25.152 0 0 0 25.168-25.168V248.344a25.168 25.168 0 0 0-25.168-25.176z m-8.08 544.168H217.664V258h588.008v509.336z" p-id="2702"></path><path d="M406.752 454.168a44.24 44.24 0 1 0-0.008-88.48 44.24 44.24 0 0 0 0.008 88.48zM474.72 611.368l-67.968-94.376-110.584 158.336h442.328L605.8 426.52z" p-id="2703"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/polygon.svg b/src/fabric-editor/assets/icon/layer/polygon.svg
new file mode 100644
index 0000000..d3130ed
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/polygon.svg
@@ -0,0 +1 @@
+<svg t="1650874633978" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2032" width="16" height="16"><path d="M161.152 398.016l134.016 412.416h433.664l134.016-412.416L512 143.104 161.152 398.08zM512 64l426.048 309.568-162.752 500.864H248.704L85.952 373.568 512 64z" p-id="2033"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/rect.svg b/src/fabric-editor/assets/icon/layer/rect.svg
new file mode 100644
index 0000000..589a99e
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/rect.svg
@@ -0,0 +1 @@
+<svg t="1650855811131" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18499" width="16" height="16"><path d="M864 896H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h704a32 32 0 0 1 32 32v704a32 32 0 0 1-32 32zM192 832h640V192H192v640z" p-id="18500"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/textbox.svg b/src/fabric-editor/assets/icon/layer/textbox.svg
new file mode 100644
index 0000000..145895a
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/textbox.svg
@@ -0,0 +1 @@
+<svg t="1650854954008" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14038" width="16" height="16"><path d="M720.832 692.352h-12.64L530.208 260.448a19.968 19.968 0 0 0-36.416 0L316.608 692.352h-13.44c-7.904 0-15.04 3.968-17.408 11.072a19.872 19.872 0 0 0 18.208 27.68h56.96c9.504 0 18.208-6.336 19.776-15.808a18.752 18.752 0 0 0-15.808-21.344l36.384-87.808h159.776l34.816 87.808a18.88 18.88 0 0 0-15.808 21.344c1.568 9.504 10.272 15.808 19.776 15.808h121.024c7.904 0 15.04-3.968 17.408-11.072a19.2 19.2 0 0 0-17.408-27.68z m-306.112-125.76l64.864-158.208 64.864 158.208H414.72z m-246.816-80.704c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0-75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 151.872c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m11.872-216.736a12.16 12.16 0 0 0 11.872-11.872v-23.744c-0.8-6.336-5.536-11.072-11.872-11.072s-11.872 5.536-11.872 11.872v23.744c0 6.336 4.736 11.072 11.872 11.072z m-11.872 292.672c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 75.936c0 7.104 4.736 11.872 11.872 11.872a12.16 12.16 0 0 0 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m665.248-227.808c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0-75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 151.872c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m11.072-216.736a12.16 12.16 0 0 0 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744c0.8 7.104 5.536 11.872 11.872 11.872z m-11.072 292.672c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m-347.264 119.456h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m-75.936 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m151.872 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m-228.608 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-4.736-11.872-11.872-11.872z m-75.136-7.904H222.496v-11.072a12.48 12.48 0 0 0-12.672-12.64h-18.976v-37.984c0-7.104-5.536-12.64-11.872-12.64s-11.872 5.536-11.872 12.64v37.984h-10.272a12.512 12.512 0 0 0-12.672 12.64v52.992a12.48 12.48 0 0 0 12.672 12.64h52.992a12.512 12.512 0 0 0 12.672-12.64v-11.072h35.584c7.104-1.568 12.672-7.904 12.672-14.24 0-7.104-12.672-16.608-12.672-16.608z m379.68 7.904h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m75.936 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-4.736-11.872-11.872-11.872z m-251.52-642.304h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m-75.936 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m151.872 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m-227.808 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-7.104 0.8-11.872 5.536-11.872 12.672s4.736 11.072 11.872 11.072zM257.28 167.904H221.696v-11.072a12.512 12.512 0 0 0-12.672-12.672H156.832a12.512 12.512 0 0 0-12.672 12.672v52.992c0 7.104 5.536 12.672 12.672 12.672h11.072v35.584c1.568 7.104 7.904 12.672 14.24 12.672s16.608-12.672 16.608-12.672V222.496h11.072a12.512 12.512 0 0 0 12.672-12.672v-18.976h35.584a12.16 12.16 0 0 0 11.872-11.872 12.224 12.224 0 0 0-12.672-11.072z m356.768 22.944h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m76.736 0h23.744c6.336 0 11.072-4.736 11.072-11.072s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.072 12.64 11.072z m176.384-46.656h-52.992a12.48 12.48 0 0 0-12.64 12.672v11.072h-35.584c-7.104 1.568-12.64 7.904-12.64 14.24s12.64 16.608 12.64 16.608h35.584v11.072c0 7.104 5.536 12.672 12.64 12.672h18.976v37.984c0 7.104 5.536 12.672 11.872 12.672s11.872-5.536 11.872-12.672V222.528h11.072a12.48 12.48 0 0 0 12.64-12.672V156.864a13.76 13.76 0 0 0-13.44-12.672z m0 657.312h-11.072v-35.584c-1.568-7.104-7.904-12.64-14.24-12.64-7.104 0-16.608 12.64-16.608 12.64v35.584h-11.072a12.48 12.48 0 0 0-12.64 12.64v18.976h-35.584c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h35.584v11.072a12.48 12.48 0 0 0 12.64 12.64h52.992a12.48 12.48 0 0 0 12.64-12.64v-52.992c0-7.904-5.536-13.44-12.64-13.44z" p-id="14039"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/triangle.svg b/src/fabric-editor/assets/icon/layer/triangle.svg
new file mode 100644
index 0000000..352fc88
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/triangle.svg
@@ -0,0 +1 @@
+<svg t="1650874633978" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2032" width="16" height="16"><path d="M928.64 896a2.144 2.144 0 0 1-0.64 0H96a32.032 32.032 0 0 1-27.552-48.288l416-704c11.488-19.456 43.552-19.456 55.104 0l413.152 699.2A31.936 31.936 0 0 1 928.64 896zM152.064 832h719.84L512 222.912 152.064 832z" p-id="2033"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/up.svg b/src/fabric-editor/assets/icon/layer/up.svg
new file mode 100644
index 0000000..bfe6a59
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/up.svg
@@ -0,0 +1 @@
+<svg t="1650442206559" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1799" width="12" height="12"><path d="M876.2 434.3L536.7 94.9c-6.6-6.6-15.5-10.3-24.7-10.3-9.3 0-18.2 3.7-24.7 10.3L147.8 434.3c-13.7 13.7-13.7 35.8 0 49.5 13.7 13.7 35.8 13.7 49.5 0L477 204.1v700.2c0 19.3 15.7 35 35 35s35-15.7 35-35V204.1l279.7 279.7c6.8 6.8 15.8 10.3 24.7 10.3s17.9-3.4 24.7-10.3c13.7-13.7 13.7-35.8 0.1-49.5z" p-id="1800"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/layer/upTop.svg b/src/fabric-editor/assets/icon/layer/upTop.svg
new file mode 100644
index 0000000..f118616
--- /dev/null
+++ b/src/fabric-editor/assets/icon/layer/upTop.svg
@@ -0,0 +1 @@
+<svg t="1650442106652" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1839" width="11" height="11"><path d="M548.352 219.648a58.88 58.88 0 0 0-16.896-10.752 51.2 51.2 0 0 0-38.912 0 58.88 58.88 0 0 0-16.896 10.752l-256 256a51.2 51.2 0 0 0 72.704 72.704L460.8 379.392V972.8a51.2 51.2 0 0 0 102.4 0V379.392l168.448 168.96a51.2 51.2 0 0 0 72.704-72.704zM972.8 0H51.2a51.2 51.2 0 0 0 0 102.4h921.6a51.2 51.2 0 0 0 0-102.4z" p-id="1840" ></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/left.svg b/src/fabric-editor/assets/icon/left.svg
new file mode 100644
index 0000000..87e5ca7
--- /dev/null
+++ b/src/fabric-editor/assets/icon/left.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113281502" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4028" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M234.666667 170.666667a21.333333 21.333333 0 0 1 21.333333 21.333333v640a21.333333 21.333333 0 1 1-42.666667 0v-640a21.333333 21.333333 0 0 1 21.333334-21.333333zM810.666667 554.666667a42.666667 42.666667 0 0 1 42.666666 42.666666v85.333334a42.666667 42.666667 0 0 1-42.666666 42.666666H384a42.666667 42.666667 0 0 1-42.666667-42.666666v-85.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h426.666667z m-213.333334-256a42.666667 42.666667 0 0 1 42.666667 42.666666v85.333334a42.666667 42.666667 0 0 1-42.666667 42.666666H384a42.666667 42.666667 0 0 1-42.666667-42.666666V341.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h213.333333z" fill="#666666" p-id="4029"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/proIcon.png b/src/fabric-editor/assets/icon/proIcon.png
new file mode 100644
index 0000000..007f1d7
--- /dev/null
+++ b/src/fabric-editor/assets/icon/proIcon.png
Binary files differ
diff --git a/src/fabric-editor/assets/icon/right.svg b/src/fabric-editor/assets/icon/right.svg
new file mode 100644
index 0000000..5ccd2fd
--- /dev/null
+++ b/src/fabric-editor/assets/icon/right.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113301433" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4204" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M832 170.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v640a21.333333 21.333333 0 1 0 42.666666 0v-640a21.333333 21.333333 0 0 0-21.333333-21.333333zM256 554.666667a42.666667 42.666667 0 0 0-42.666667 42.666666v85.333334a42.666667 42.666667 0 0 0 42.666667 42.666666h426.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-85.333334a42.666667 42.666667 0 0 0-42.666666-42.666666H256z m213.333333-256a42.666667 42.666667 0 0 0-42.666666 42.666666v85.333334a42.666667 42.666667 0 0 0 42.666666 42.666666h213.333334a42.666667 42.666667 0 0 0 42.666666-42.666666V341.333333a42.666667 42.666667 0 0 0-42.666666-42.666666h-213.333334z" fill="#666666" p-id="4205"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/sx.svg b/src/fabric-editor/assets/icon/sx.svg
new file mode 100644
index 0000000..1b76527
--- /dev/null
+++ b/src/fabric-editor/assets/icon/sx.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113318915" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4908" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M320 170.666667a21.333333 21.333333 0 0 1 21.333333 21.333333v640a21.333333 21.333333 0 1 1-42.666666 0v-640a21.333333 21.333333 0 0 1 21.333333-21.333333z m384 0a21.333333 21.333333 0 0 1 21.333333 21.333333v640a21.333333 21.333333 0 1 1-42.666666 0v-640a21.333333 21.333333 0 0 1 21.333333-21.333333zM554.666667 256a42.666667 42.666667 0 0 1 42.666666 42.666667v426.666666a42.666667 42.666667 0 0 1-42.666666 42.666667h-85.333334a42.666667 42.666667 0 0 1-42.666666-42.666667V298.666667a42.666667 42.666667 0 0 1 42.666666-42.666667h85.333334z" fill="#666666" p-id="4909"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/sy.svg b/src/fabric-editor/assets/icon/sy.svg
new file mode 100644
index 0000000..683dfc2
--- /dev/null
+++ b/src/fabric-editor/assets/icon/sy.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113367636" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5612" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 320a21.333333 21.333333 0 0 1-21.333333 21.333333h-640a21.333333 21.333333 0 0 1 0-42.666666h640a21.333333 21.333333 0 0 1 21.333333 21.333333z m0 384a21.333333 21.333333 0 0 1-21.333333 21.333333h-640a21.333333 21.333333 0 1 1 0-42.666666h640a21.333333 21.333333 0 0 1 21.333333 21.333333zM768 554.666667a42.666667 42.666667 0 0 1-42.666667 42.666666H298.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-85.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h426.666666a42.666667 42.666667 0 0 1 42.666667 42.666666v85.333334z" fill="#666666" p-id="5613"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/barCode.svg b/src/fabric-editor/assets/icon/tools/barCode.svg
new file mode 100644
index 0000000..1b8aed7
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/barCode.svg
@@ -0,0 +1,15 @@
+ <svg
+          t="1717679973041"
+          class="icon"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="5747"
+          width="20"
+          height="20"
+        >
+          <path
+            d="M876.893 63.84h20.44c34.501 0 62.49 30.402 62.49 64.919V229.7h-0.13c0 16.569-11.355 22.409-22.855 22.409s-22.854-7.734-22.854-19.234c0-0.211-3.92-0.373-3.92-0.583v-82.72c0-23-10.56-35.975-33.56-35.975h-61.47c-0.21 0-1.36-2.786-1.555-2.786-11.5 0-21.817-12.213-21.817-23.713s18.885-20.813 18.885-20.813V63.84h66.345zM213.45 66.286V63.84h-86.914c-34.5 0-62.36 30.402-62.36 64.919V229.7c0 16.569 9.33 22.409 20.83 22.409s24.927-8.528 24.927-20.028c0-0.211 4.001 0.421 4.001 0.21v-82.72c0-23 10.432-35.974 33.432-35.974h63.008c0.194 0 0.372-2.786 0.583-2.786 11.5 0 22.06-10.772 22.06-22.263 0-11.5-2.98-22.263-19.567-22.263z m16.587 500.74V246.27h-33.173v530.764h33.173V567.026zM109.933 795.141c0-11.5-11.37-21.576-22.87-21.576s-22.888 3.468-22.888 20.053v105.638c0 34.5 27.86 60.224 62.36 60.224h83.837c0.194 0 0.372 0.679 0.583 0.679 11.5 0 22.06-9.038 22.06-20.538s-2.98-20.538-19.566-20.538v7.224h-66.344v-6.737c-33.173-2.624-33.173-19.923-33.173-41.14v-82.704c0.001-0.213-3.999-0.39-3.999-0.585z m186.448-228.115V246.27h-16.586V694.1h16.586V567.026z m16.585 160.249h-33.172v49.758h33.172v-49.758z m66.344-160.249V246.27h-16.585V694.1h16.585V567.026z m16.586 160.249h-33.171v49.758h33.171v-49.758z m82.931-160.249V246.27h-66.344V694.1h66.344V567.026z m0 210.007v-49.758h-33.173v49.758h33.173z m132.688-210.007V246.27h-49.758V694.1h49.758V567.026z m-82.93 210.007h33.172v-49.758h-33.172v49.758zM661.273 566.41V246.27H644.69V694.1h16.584V566.41z m-33.171 210.623h33.171v-49.758h-33.171v49.758zM744.205 566.41V246.27h-16.587V694.1h16.587V566.41z m-33.173 210.623h33.173v-49.758h-33.173v49.758z m116.103-210.007V246.27h-33.172v530.764h33.172V567.026z m70.198 392.454c34.5 0 62.489-25.723 62.489-60.224V793.618h-0.13c0-16.585-9.33-20.053-20.83-20.053s-24.88 9.688-24.88 21.188c0 0.226-3.918 0.76-3.918 0.97v82.705c0 20.7 0 37.738-33.172 40.978v6.899h-66.345v-7.224s-18.885 9.33-18.885 20.83 10.317 20.246 21.817 20.246c0.193 0 1.344-0.68 1.555-0.68h82.299z"
+            p-id="5748"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/circle.svg b/src/fabric-editor/assets/icon/tools/circle.svg
new file mode 100644
index 0000000..592c044
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/circle.svg
@@ -0,0 +1,14 @@
+ <svg
+          t="1650855860236"
+         
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="19440"
+         
+        >
+          <path
+            d="M512 928C282.624 928 96 741.376 96 512S282.624 96 512 96s416 186.624 416 416-186.624 416-416 416z m0-768C317.92 160 160 317.92 160 512s157.92 352 352 352 352-157.92 352-352S706.08 160 512 160z"
+            p-id="19441"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/draw1.svg b/src/fabric-editor/assets/icon/tools/draw1.svg
new file mode 100644
index 0000000..7423a0f
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/draw1.svg
@@ -0,0 +1,20 @@
+ <svg
+          t="1673022047861"
+        
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="4206"
+          
+        >
+          <path
+            d="M187.733333 1024h-170.666666c-10.24 0-17.066667-6.826667-17.066667-17.066667v-170.666666c0-10.24 6.826667-17.066667 17.066667-17.066667h170.666666c10.24 0 17.066667 6.826667 17.066667 17.066667v170.666666c0 10.24-6.826667 17.066667-17.066667 17.066667zM34.133333 989.866667h136.533334v-136.533334H34.133333v136.533334zM1006.933333 204.8h-170.666666c-10.24 0-17.066667-6.826667-17.066667-17.066667v-170.666666c0-10.24 6.826667-17.066667 17.066667-17.066667h170.666666c10.24 0 17.066667 6.826667 17.066667 17.066667v170.666666c0 10.24-6.826667 17.066667-17.066667 17.066667zM853.333333 170.666667h136.533334V34.133333h-136.533334v136.533334z"
+            fill=""
+            p-id="4207"
+          ></path>
+          <path
+            d="M187.733333 853.333333c-3.413333 0-10.24 0-13.653333-3.413333-6.826667-6.826667-6.826667-17.066667 0-23.893333l648.533333-648.533334c6.826667-6.826667 17.066667-6.826667 23.893334 0s6.826667 17.066667 0 23.893334l-648.533334 648.533333c0 3.413333-6.826667 3.413333-10.24 3.413333z"
+            fill=""
+            p-id="4208"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/draw2.svg b/src/fabric-editor/assets/icon/tools/draw2.svg
new file mode 100644
index 0000000..24c4a9e
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/draw2.svg
@@ -0,0 +1,15 @@
+ <svg
+          t="1673026778912"
+          
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="2659"
+          
+        >
+          <path
+            d="M320 738.133333L827.733333 230.4l-29.866666-29.866667L290.133333 708.266667v-268.8h-42.666666v341.333333h341.333333v-42.666667H320z"
+            fill="#444444"
+            p-id="2660"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/draw3.svg b/src/fabric-editor/assets/icon/tools/draw3.svg
new file mode 100644
index 0000000..8099d74
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/draw3.svg
@@ -0,0 +1,15 @@
+ <svg
+          t="1715323097309"
+          class="icon"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="24572"
+          width="20"
+          height="20"
+        >
+          <path
+            d="M485.954269 735.978673 643.38051 811.107852C688.831327 832.798434 686.17686 860.459274 635.3838 871.903075L81.378406 996.721881C31.19882 1008.027444-1.313538 976.266799 10.130269 925.473745L134.949081 371.468357C146.254656 321.288783 173.611855 317.095036 195.744311 363.471653L270.873453 520.897858 903.670271 62.052983C986.301645 2.136458 1004.805285 20.426857 944.799125 103.181838L485.954269 735.978673Z"
+            p-id="24573"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/draw4.svg b/src/fabric-editor/assets/icon/tools/draw4.svg
new file mode 100644
index 0000000..86a4bfc
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/draw4.svg
@@ -0,0 +1,15 @@
+  <svg
+          t="1650874633978"
+          class="icon"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="203255"
+          width="26"
+          height="26"
+        >
+          <path
+            d="M161.152 398.016l134.016 412.416h433.664l134.016-412.416L512 143.104 161.152 398.08zM512 64l426.048 309.568-162.752 500.864H248.704L85.952 373.568 512 64z"
+            p-id="203355"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/polygon.svg b/src/fabric-editor/assets/icon/tools/polygon.svg
new file mode 100644
index 0000000..62fe06c
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/polygon.svg
@@ -0,0 +1,14 @@
+ <svg
+          t="1650874633978"
+          
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="2032"
+        
+        >
+          <path
+            d="M161.152 398.016l134.016 412.416h433.664l134.016-412.416L512 143.104 161.152 398.08zM512 64l426.048 309.568-162.752 500.864H248.704L85.952 373.568 512 64z"
+            p-id="2033"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/qrCode.svg b/src/fabric-editor/assets/icon/tools/qrCode.svg
new file mode 100644
index 0000000..b148393
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/qrCode.svg
@@ -0,0 +1,23 @@
+ <svg
+          t="1717679888665"
+          class="icon"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          p-id="4558"
+          xmlns="http://www.w3.org/2000/svg"
+          width="20"
+          height="20"
+        >
+          <path
+            d="M386.612245 470.204082H177.632653c-45.97551 0-83.591837-37.616327-83.591837-83.591837V177.632653c0-45.97551 37.616327-83.591837 83.591837-83.591837h208.979592c45.97551 0 83.591837 37.616327 83.591837 83.591837v208.979592c0 45.97551-37.616327 83.591837-83.591837 83.591837zM177.632653 135.836735c-22.987755 0-41.795918 18.808163-41.795918 41.795918v208.979592c0 22.987755 18.808163 41.795918 41.795918 41.795918h208.979592c22.987755 0 41.795918-18.808163 41.795918-41.795918V177.632653c0-22.987755-18.808163-41.795918-41.795918-41.795918H177.632653zM846.367347 470.204082h-208.979592c-45.97551 0-83.591837-37.616327-83.591837-83.591837V177.632653c0-45.97551 37.616327-83.591837 83.591837-83.591837h208.979592c45.97551 0 83.591837 37.616327 83.591837 83.591837v208.979592c0 45.97551-37.616327 83.591837-83.591837 83.591837z m-208.979592-334.367347c-22.987755 0-41.795918 18.808163-41.795918 41.795918v208.979592c0 22.987755 18.808163 41.795918 41.795918 41.795918h208.979592c22.987755 0 41.795918-18.808163 41.795918-41.795918V177.632653c0-22.987755-18.808163-41.795918-41.795918-41.795918h-208.979592z"
+            p-id="4559"
+          ></path>
+          <path
+            d="M773.22449 344.816327h-62.693878c-17.240816 0-31.346939-14.106122-31.346939-31.346939V250.77551c0-17.240816 14.106122-31.346939 31.346939-31.346939h62.693878c17.240816 0 31.346939 14.106122 31.346939 31.346939v62.693878c0 17.240816-14.106122 31.346939-31.346939 31.346939zM313.469388 344.816327H250.77551c-17.240816 0-31.346939-14.106122-31.346939-31.346939V250.77551c0-17.240816 14.106122-31.346939 31.346939-31.346939h62.693878c17.240816 0 31.346939 14.106122 31.346939 31.346939v62.693878c0 17.240816-14.106122 31.346939-31.346939 31.346939zM313.469388 804.571429H250.77551c-17.240816 0-31.346939-14.106122-31.346939-31.346939v-62.693878c0-17.240816 14.106122-31.346939 31.346939-31.346939h62.693878c17.240816 0 31.346939 14.106122 31.346939 31.346939v62.693878c0 17.240816-14.106122 31.346939-31.346939 31.346939zM574.693878 929.959184c-11.493878 0-20.897959-9.404082-20.89796-20.89796v-271.673469c0-45.97551 37.616327-83.591837 83.591837-83.591837h41.795918c45.97551 0 83.591837 37.616327 83.591837 83.591837v62.693878c0 22.987755 18.808163 41.795918 41.795919 41.795918h41.795918c22.987755 0 41.795918-18.808163 41.795918-41.795918v-125.387755c0-11.493878 9.404082-20.897959 20.897959-20.89796s20.897959 9.404082 20.89796 20.89796v125.387755c0 45.97551-37.616327 83.591837-83.591837 83.591836h-41.795918c-45.97551 0-83.591837-37.616327-83.591837-83.591836v-62.693878c0-22.987755-18.808163-41.795918-41.795919-41.795918h-41.795918c-22.987755 0-41.795918 18.808163-41.795918 41.795918v271.673469c0 11.493878-9.404082 20.897959-20.897959 20.89796zM909.061224 929.959184h-167.183673c-11.493878 0-20.897959-9.404082-20.897959-20.89796s9.404082-20.897959 20.897959-20.897959h167.183673c11.493878 0 20.897959 9.404082 20.89796 20.897959s-9.404082 20.897959-20.89796 20.89796z"
+            p-id="4560"
+          ></path>
+          <path
+            d="M386.612245 929.959184H177.632653c-45.97551 0-83.591837-37.616327-83.591837-83.591837v-208.979592c0-45.97551 37.616327-83.591837 83.591837-83.591837h208.979592c45.97551 0 83.591837 37.616327 83.591837 83.591837v208.979592c0 45.97551-37.616327 83.591837-83.591837 83.591837z m-208.979592-334.367347c-22.987755 0-41.795918 18.808163-41.795918 41.795918v208.979592c0 22.987755 18.808163 41.795918 41.795918 41.795918h208.979592c22.987755 0 41.795918-18.808163 41.795918-41.795918v-208.979592c0-22.987755-18.808163-41.795918-41.795918-41.795918H177.632653z"
+            p-id="4561"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/rect.svg b/src/fabric-editor/assets/icon/tools/rect.svg
new file mode 100644
index 0000000..77d1723
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/rect.svg
@@ -0,0 +1,14 @@
+ <svg
+          t="1650855811131"
+        
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="18499"
+         
+        >
+          <path
+            d="M864 896H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h704a32 32 0 0 1 32 32v704a32 32 0 0 1-32 32zM192 832h640V192H192v640z"
+            p-id="18500"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/text.svg b/src/fabric-editor/assets/icon/tools/text.svg
new file mode 100644
index 0000000..212d67d
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/text.svg
@@ -0,0 +1,12 @@
+ <svg
+          t="1650875455324"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="5401"
+        >
+          <path
+            d="M213.333333 209.92v128h85.333334v-42.666667h170.666666v433.493334H384.853333v85.333333h256v-85.333333H554.666667V295.253333h170.666666v42.666667h85.333334v-128H213.333333z"
+            p-id="5402"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/textBox.svg b/src/fabric-editor/assets/icon/tools/textBox.svg
new file mode 100644
index 0000000..27d1fcb
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/textBox.svg
@@ -0,0 +1,12 @@
+ <svg
+          t="1650854954008"
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="14038"
+        >
+          <path
+            d="M720.832 692.352h-12.64L530.208 260.448a19.968 19.968 0 0 0-36.416 0L316.608 692.352h-13.44c-7.904 0-15.04 3.968-17.408 11.072a19.872 19.872 0 0 0 18.208 27.68h56.96c9.504 0 18.208-6.336 19.776-15.808a18.752 18.752 0 0 0-15.808-21.344l36.384-87.808h159.776l34.816 87.808a18.88 18.88 0 0 0-15.808 21.344c1.568 9.504 10.272 15.808 19.776 15.808h121.024c7.904 0 15.04-3.968 17.408-11.072a19.2 19.2 0 0 0-17.408-27.68z m-306.112-125.76l64.864-158.208 64.864 158.208H414.72z m-246.816-80.704c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0-75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 151.872c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m11.872-216.736a12.16 12.16 0 0 0 11.872-11.872v-23.744c-0.8-6.336-5.536-11.072-11.872-11.072s-11.872 5.536-11.872 11.872v23.744c0 6.336 4.736 11.072 11.872 11.072z m-11.872 292.672c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 75.936c0 7.104 4.736 11.872 11.872 11.872a12.16 12.16 0 0 0 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m665.248-227.808c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0-75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 151.872c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m11.072-216.736a12.16 12.16 0 0 0 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744c0.8 7.104 5.536 11.872 11.872 11.872z m-11.072 292.672c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m0 75.936c0 6.336 5.536 11.872 11.872 11.872s11.872-5.536 11.872-11.872v-23.744c0-6.336-5.536-11.872-11.872-11.872s-11.872 5.536-11.872 11.872v23.744z m-347.264 119.456h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m-75.936 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m151.872 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m-228.608 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-4.736-11.872-11.872-11.872z m-75.136-7.904H222.496v-11.072a12.48 12.48 0 0 0-12.672-12.64h-18.976v-37.984c0-7.104-5.536-12.64-11.872-12.64s-11.872 5.536-11.872 12.64v37.984h-10.272a12.512 12.512 0 0 0-12.672 12.64v52.992a12.48 12.48 0 0 0 12.672 12.64h52.992a12.512 12.512 0 0 0 12.672-12.64v-11.072h35.584c7.104-1.568 12.672-7.904 12.672-14.24 0-7.104-12.672-16.608-12.672-16.608z m379.68 7.904h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872z m75.936 0h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h23.744c6.336 0 11.872-5.536 11.872-11.872s-4.736-11.872-11.872-11.872z m-251.52-642.304h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m-75.936 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m151.872 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m-227.808 0h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-7.104 0.8-11.872 5.536-11.872 12.672s4.736 11.072 11.872 11.072zM257.28 167.904H221.696v-11.072a12.512 12.512 0 0 0-12.672-12.672H156.832a12.512 12.512 0 0 0-12.672 12.672v52.992c0 7.104 5.536 12.672 12.672 12.672h11.072v35.584c1.568 7.104 7.904 12.672 14.24 12.672s16.608-12.672 16.608-12.672V222.496h11.072a12.512 12.512 0 0 0 12.672-12.672v-18.976h35.584a12.16 12.16 0 0 0 11.872-11.872 12.224 12.224 0 0 0-12.672-11.072z m356.768 22.944h23.744c6.336 0 11.872-5.536 11.872-11.872s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872z m76.736 0h23.744c6.336 0 11.072-4.736 11.072-11.072s-5.536-11.872-11.872-11.872h-23.744c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.072 12.64 11.072z m176.384-46.656h-52.992a12.48 12.48 0 0 0-12.64 12.672v11.072h-35.584c-7.104 1.568-12.64 7.904-12.64 14.24s12.64 16.608 12.64 16.608h35.584v11.072c0 7.104 5.536 12.672 12.64 12.672h18.976v37.984c0 7.104 5.536 12.672 11.872 12.672s11.872-5.536 11.872-12.672V222.528h11.072a12.48 12.48 0 0 0 12.64-12.672V156.864a13.76 13.76 0 0 0-13.44-12.672z m0 657.312h-11.072v-35.584c-1.568-7.104-7.904-12.64-14.24-12.64-7.104 0-16.608 12.64-16.608 12.64v35.584h-11.072a12.48 12.48 0 0 0-12.64 12.64v18.976h-35.584c-6.336 0-11.872 5.536-11.872 11.872s5.536 11.872 11.872 11.872h35.584v11.072a12.48 12.48 0 0 0 12.64 12.64h52.992a12.48 12.48 0 0 0 12.64-12.64v-52.992c0-7.904-5.536-13.44-12.64-13.44z"
+            p-id="14039"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/tools/triangle.svg b/src/fabric-editor/assets/icon/tools/triangle.svg
new file mode 100644
index 0000000..c984929
--- /dev/null
+++ b/src/fabric-editor/assets/icon/tools/triangle.svg
@@ -0,0 +1,14 @@
+ <svg
+          t="1650874633978"
+         
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="2032"
+          
+        >
+          <path
+            d="M928.64 896a2.144 2.144 0 0 1-0.64 0H96a32.032 32.032 0 0 1-27.552-48.288l416-704c11.488-19.456 43.552-19.456 55.104 0l413.152 699.2A31.936 31.936 0 0 1 928.64 896zM152.064 832h719.84L512 222.912 152.064 832z"
+            p-id="2033"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/top.svg b/src/fabric-editor/assets/icon/top.svg
new file mode 100644
index 0000000..f3e7fff
--- /dev/null
+++ b/src/fabric-editor/assets/icon/top.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721113325398" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5084" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 234.666667a21.333333 21.333333 0 0 1-21.333333 21.333333h-640a21.333333 21.333333 0 0 1 0-42.666667h640a21.333333 21.333333 0 0 1 21.333333 21.333334zM469.333333 810.666667a42.666667 42.666667 0 0 1-42.666666 42.666666H341.333333a42.666667 42.666667 0 0 1-42.666666-42.666666V384a42.666667 42.666667 0 0 1 42.666666-42.666667h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v426.666667z m256-213.333334a42.666667 42.666667 0 0 1-42.666666 42.666667h-85.333334a42.666667 42.666667 0 0 1-42.666666-42.666667V384a42.666667 42.666667 0 0 1 42.666666-42.666667h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v213.333333z" fill="#666666" p-id="5085"></path></svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/zoom/big.svg b/src/fabric-editor/assets/icon/zoom/big.svg
new file mode 100644
index 0000000..e508fb9
--- /dev/null
+++ b/src/fabric-editor/assets/icon/zoom/big.svg
@@ -0,0 +1,20 @@
+ <svg
+          t="1650853919128"
+        
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="1271"
+         
+        >
+          <path
+            d="M970.837333 919.850667l-205.696-205.653334A382.421333 382.421333 0 0 0 853.333333 469.333333a384 384 0 0 0-384-384 384 384 0 0 0-384 384 384 384 0 0 0 384 384 382.421333 382.421333 0 0 0 244.906667-88.192l205.653333 205.653334a36.053333 36.053333 0 0 0 50.986667 0 36.266667 36.266667 0 0 0-0.042667-50.944z m-380.117333-162.986667c-38.4 16.256-79.189333 24.448-121.386667 24.448a311.296 311.296 0 0 1-220.586666-91.392A311.296 311.296 0 0 1 157.312 469.333333 311.296 311.296 0 0 1 248.746667 248.746667 311.296 311.296 0 0 1 469.333333 157.354667a311.296 311.296 0 0 1 220.586667 91.392A311.296 311.296 0 0 1 781.354667 469.333333a311.296 311.296 0 0 1-91.392 220.586667 310.186667 310.186667 0 0 1-99.242667 66.901333z"
+            fill="#595959"
+            p-id="1272"
+          ></path>
+          <path
+            d="M652.672 431.829333h-147.84V292.010667a35.968 35.968 0 1 0-71.978667 0v139.818666H292.010667a35.968 35.968 0 1 0 0 72.021334h140.8v140.8a35.968 35.968 0 1 0 72.021333 0v-140.8h147.84a35.968 35.968 0 1 0 0-72.021334z"
+            fill="#595959"
+            p-id="1273"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/assets/icon/zoom/small.svg b/src/fabric-editor/assets/icon/zoom/small.svg
new file mode 100644
index 0000000..7ed76c6
--- /dev/null
+++ b/src/fabric-editor/assets/icon/zoom/small.svg
@@ -0,0 +1,20 @@
+ <svg
+          t="1650853934351"
+         
+          viewBox="0 0 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          p-id="1470"
+         
+        >
+          <path
+            d="M970.837333 919.850667l-205.696-205.653334A382.421333 382.421333 0 0 0 853.333333 469.333333a384 384 0 0 0-384-384 384 384 0 0 0-384 384 384 384 0 0 0 384 384 382.421333 382.421333 0 0 0 244.906667-88.192l205.653333 205.653334a36.053333 36.053333 0 0 0 50.986667 0 36.266667 36.266667 0 0 0-0.042667-50.944z m-380.117333-162.986667c-38.4 16.256-79.189333 24.448-121.386667 24.448a311.296 311.296 0 0 1-220.586666-91.392A311.296 311.296 0 0 1 157.312 469.333333 311.296 311.296 0 0 1 248.746667 248.746667 311.296 311.296 0 0 1 469.333333 157.354667a311.296 311.296 0 0 1 220.586667 91.392A311.296 311.296 0 0 1 781.354667 469.333333a311.296 311.296 0 0 1-91.392 220.586667 310.186667 310.186667 0 0 1-99.242667 66.901333z"
+            fill="#595959"
+            p-id="1471"
+          ></path>
+          <path
+            d="M652.672 431.829333H292.010667a35.968 35.968 0 1 0 0 72.021334h360.661333a35.968 35.968 0 1 0 0-72.021334z"
+            fill="#595959"
+            p-id="1472"
+          ></path>
+        </svg>
\ No newline at end of file
diff --git a/src/fabric-editor/components/admin.vue b/src/fabric-editor/components/admin.vue
new file mode 100644
index 0000000..a919f92
--- /dev/null
+++ b/src/fabric-editor/components/admin.vue
@@ -0,0 +1,100 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-09 13:23:07
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-10 20:17:32
+ * @Description: 绠$悊鍛樻ā寮�
+-->
+
+<template>
+  <div style="display: inline-block" v-if="route.query.admin">
+    <Dropdown style="margin-left: 10px" @on-click="adminOperation">
+      <Button type="primary">
+        {{ $t('admin.btnTitle') }}
+        <Icon type="ios-arrow-down"></Icon>
+      </Button>
+      <template #list>
+        <DropdownMenu>
+          <DropdownItem name="saveImg">{{ $t('admin.save') }}</DropdownItem>
+          <DropdownItem name="setToken" divided>{{ $t('admin.setToken') }}</DropdownItem>
+        </DropdownMenu>
+      </template>
+    </Dropdown>
+
+    <Modal v-model="modal" :title="$t('admin.setToken')" @on-ok="setTokenHandel">
+      <Form :label-width="50">
+        <FormItem label="Token">
+          <Input v-model="token" type="textarea"></Input>
+        </FormItem>
+      </Form>
+    </Modal>
+  </div>
+</template>
+
+<script setup name="ImportTmpl">
+import { getToken, setToken } from '@/api/admin';
+import { Message, Modal, Input } from 'view-ui-plus';
+import useSelect from '@/hooks/select';
+import useAdmin from '@/hooks/useAdmin';
+const { updataTemplHander, createdTemplHander } = useAdmin();
+import { useRoute } from 'vue-router';
+
+const { t } = useSelect();
+
+const route = useRoute();
+
+const modal = ref(false);
+const token = ref('');
+
+const adminOperation = (name) => {
+  const handerMap = {
+    setToken: showAdmin,
+    saveImg: updataTemp,
+  };
+
+  handerMap[name]();
+};
+const showAdmin = async () => {
+  token.value = getToken();
+  modal.value = true;
+};
+
+const updataTemp = () => {
+  // 鏇存柊妯℃澘
+  if (route.query.tempId) {
+    updataTemplHander(route.query.tempId);
+  } else {
+    // 鏂板
+    addTempl();
+  }
+};
+
+const templName = ref('');
+const addTempl = () => {
+  Modal.confirm({
+    title: t('admin.addTempl'),
+    render: (h) => {
+      return h(Input, {
+        size: 'large',
+        modelValue: templName,
+        autofocus: true,
+        placeholder: t('admin.addTemplPlaceholder'),
+      });
+    },
+    onOk: async () => {
+      if (templName.value === '') {
+        Message.warning(t('admin.addTemplCheckTip'));
+        return;
+      }
+      await createdTemplHander(templName.value);
+    },
+  });
+};
+
+const setTokenHandel = () => {
+  setToken(String(token.value));
+  Message.success('澶嶅埗鎴愬姛');
+};
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/align.vue b/src/fabric-editor/components/align.vue
new file mode 100644
index 0000000..72654b0
--- /dev/null
+++ b/src/fabric-editor/components/align.vue
@@ -0,0 +1,110 @@
+<template>
+  <div v-if="isMultiple" class="attr-item-box">
+    <!-- <h3>瀵归綈</h3> -->
+    <Divider plain orientation="left"><h4>瀵归綈</h4></Divider>
+    <div class="bg-item">
+      <!-- 姘村钩瀵归綈 -->
+      <Tooltip :content="$t('attrSeting.align.left')">
+        <Button @click="left" size="small" type="text">
+          <leftIcon />
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.align.centerX')">
+        <Button @click="xcenter" size="small" type="text">
+          <centerxIcon />
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.align.right')">
+        <Button @click="right" size="small" type="text">
+          <rightIcon />
+        </Button>
+      </Tooltip>
+      <!-- 鍨傜洿瀵归綈 -->
+      <Tooltip :content="$t('attrSeting.align.top')">
+        <Button @click="top" size="small" type="text">
+          <topIcon />
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.align.centerY')">
+        <Button @click="ycenter" size="small" type="text">
+          <centeryIcon />
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.align.bottom')">
+        <Button @click="bottom" size="small" type="text">
+          <bottomIcon />
+        </Button>
+      </Tooltip>
+      <!-- 骞冲潎瀵归綈 -->
+      <Tooltip :content="$t('attrSeting.align.averageX')">
+        <Button @click="xequation" size="small" type="text">
+          <sxIcon />
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.align.averageY')">
+        <Button @click="yequation" size="small" type="text">
+          <syIcon />
+        </Button>
+      </Tooltip>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script name="Align" setup>
+import useSelect from '@/hooks/select';
+
+import leftIcon from '@/assets/icon/left.svg';
+import rightIcon from '@/assets/icon/right.svg';
+
+import topIcon from '@/assets/icon/top.svg';
+import bottomIcon from '@/assets/icon/bottom.svg';
+
+import sxIcon from '@/assets/icon/sx.svg';
+import syIcon from '@/assets/icon/sy.svg';
+
+import centerxIcon from '@/assets/icon/centerx.svg';
+import centeryIcon from '@/assets/icon/centery.svg';
+
+const { canvasEditor, isMultiple } = useSelect();
+
+// 宸﹀榻�
+const left = () => {
+  canvasEditor.left();
+};
+// 鍙冲榻�
+const right = () => {
+  canvasEditor.right();
+};
+// 姘村钩灞呬腑瀵归綈
+const xcenter = () => {
+  canvasEditor.xcenter();
+};
+// 鍨傜洿灞呬腑瀵归綈
+const ycenter = () => {
+  canvasEditor.ycenter();
+};
+// 椤堕儴瀵归綈
+const top = () => {
+  canvasEditor.top();
+};
+// 搴曢儴瀵归綈
+const bottom = () => {
+  canvasEditor.bottom();
+};
+// 姘村钩骞冲潎瀵归綈
+const xequation = () => {
+  canvasEditor.xequation();
+};
+// 鍨傜洿骞冲潎瀵归綈
+const yequation = () => {
+  canvasEditor.yequation();
+};
+</script>
+
+<style scoped lang="less">
+.icon {
+  width: 100%;
+  height: auto;
+}
+</style>
diff --git a/src/fabric-editor/components/attribute.vue b/src/fabric-editor/components/attribute.vue
new file mode 100644
index 0000000..86ac9e3
--- /dev/null
+++ b/src/fabric-editor/components/attribute.vue
@@ -0,0 +1,255 @@
+<template>
+  <div class="box" v-if="isOne">
+    <!-- 閫氱敤灞炴�� -->
+    <div v-show="isMatchType">
+      <Divider plain orientation="left">{{ $t('attributes.exterior') }}</Divider>
+      <!-- 澶氳竟褰㈣竟鏁� -->
+      <Row v-if="selectType === 'polygon'" :gutter="12">
+        <Col flex="0.5">
+          <InputNumber
+            v-model="baseAttr.points.length"
+            :min="3"
+            :max="30"
+            @on-change="changeEdge"
+            append="杈规暟"
+          ></InputNumber>
+        </Col>
+      </Row>
+      <!-- 棰滆壊 -->
+      <colorSelector
+        :color="baseAttr.fill"
+        @change="(value) => changeCommon('fill', value)"
+      ></colorSelector>
+    </div>
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import colorSelector from '@/components/colorSelector.vue';
+import { getPolygonVertices } from '@/utils/math';
+import InputNumber from '@/components/inputNumber';
+
+// 閫氱敤鍏冪礌
+const baseType = [
+  'text',
+  'i-text',
+  'textbox',
+  'rect',
+  'circle',
+  'triangle',
+  'polygon',
+  'image',
+  'group',
+  'line',
+  'arrow',
+  'thinTailArrow',
+];
+
+const update = getCurrentInstance();
+const { selectType, canvasEditor, isOne, isMatchType } = useSelect(baseType);
+
+const fontsList = ref([]);
+canvasEditor.getFontList().then((list) => {
+  fontsList.value = list;
+});
+
+// 鏂囧瓧鍏冪礌
+// 閫氱敤灞炴��
+const baseAttr = reactive({
+  id: '',
+  opacity: 0,
+  angle: 0,
+  fill: '#fff',
+  left: 0,
+  top: 0,
+  strokeWidth: 0,
+  strokeDashArray: [],
+  stroke: '#fff',
+  shadow: {
+    color: '#fff',
+    blur: 0,
+    offsetX: 0,
+    offsetY: 0,
+  },
+  points: {},
+  linkData: [null, null],
+});
+
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject) {
+    // base
+    baseAttr.fill = activeObject.get('fill');
+    baseAttr.points = activeObject.get('points') || {};
+  }
+};
+
+const selectCancel = () => {
+  baseAttr.fill = '';
+  update?.proxy?.$forceUpdate();
+};
+
+const init = () => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  activeObject && activeObject.set(key, value);
+  canvasEditor.canvas.renderAll();
+};
+
+// 淇敼杈规暟
+const changeEdge = (value) => {
+  const activeObjects = canvasEditor.canvas.getActiveObjects();
+  if (!activeObjects || !activeObjects.length) return;
+  activeObjects[0].set(
+    'points',
+    getPolygonVertices(value, Math.min(activeObjects[0].width, activeObjects[0].height) / 2)
+  );
+  canvasEditor.canvas.requestRenderAll();
+};
+
+onMounted(init);
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+// @import url('vue-color-gradient-picker/dist/index.css');
+:deep(.ivu-color-picker) {
+  display: block;
+}
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-divider-plain) {
+  &.ivu-divider-with-text-left {
+    margin: 10px 0;
+    font-weight: bold;
+    font-size: 16px;
+    color: #000000;
+  }
+}
+.box {
+  width: 100%;
+}
+
+.button-group {
+  display: flex;
+  width: 100%;
+  .ivu-btn,
+  .ivu-radio-wrapper {
+    flex: 1;
+  }
+}
+
+// .flex-view {
+//   width: 100%;
+//   margin-bottom: 5px;
+//   padding: 5px;
+//   display: inline-flex;
+//   justify-content: space-between;
+//   border-radius: 5px;
+//   background: #f6f7f9;
+// }
+.flex-item {
+  display: inline-flex;
+  flex: 1;
+  .label {
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    display: inline-block;
+    font-size: 14px;
+    // color: #333333;
+  }
+  .content {
+    width: 60px;
+  }
+  .slider-box {
+    width: calc(100% - 50px);
+    margin-left: 10px;
+  }
+  .left {
+    flex: 1;
+  }
+  .right {
+    flex: 1;
+    margin-left: 10px;
+    :deep(.ivu-input-number) {
+      display: block;
+      width: 100%;
+    }
+  }
+  :deep(.ivu-slider-wrap) {
+    margin: 13px 0;
+  }
+  :deep(.ivu-radio-group-button) {
+    & .ivu-radio-wrapper {
+      width: 48px;
+      line-height: 40px;
+      text-align: center;
+      svg {
+        vertical-align: baseline;
+      }
+    }
+  }
+
+  :deep(.ivu-btn-group-large) {
+    & > .ivu-btn {
+      font-size: 24px;
+    }
+  }
+
+  :deep(.ivu-radio-group-button) {
+    &.ivu-radio-group-large .ivu-radio-wrapper {
+      font-size: 24px;
+    }
+  }
+}
+
+.ivu-row {
+  margin-bottom: 8px;
+
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+
+  .content {
+    flex: 1;
+
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeBarcode.vue b/src/fabric-editor/components/attributeBarcode.vue
new file mode 100644
index 0000000..3c69993
--- /dev/null
+++ b/src/fabric-editor/components/attributeBarcode.vue
@@ -0,0 +1,315 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-06 16:27:21
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:30:43
+ * @Description: 鏉″舰鐮佹彃浠�
+-->
+
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType && isBarcode">
+    <!-- <h3>瀛椾綋灞炴��</h3> -->
+    <Divider plain orientation="left"><h4>鏉″舰鐮佸睘鎬�</h4></Divider>
+    <div>
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">浠g爜</span>
+          <div class="content">
+            <Input v-model="baseAttr.value" @on-change="changeCommon" />
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view" v-if="baseAttr.displayValue">
+        <div class="flex-item">
+          <span class="label">鏂囧瓧</span>
+          <div class="content">
+            <Input v-model="baseAttr.text" @on-change="changeCommon" />
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鏄剧ず</span>
+          <div class="content">
+            <Switch v-model="baseAttr.displayValue" @on-change="changeCommon" />
+          </div>
+        </div>
+        <div class="flex-item" v-if="baseAttr.displayValue">
+          <span class="label">鍨傜洿</span>
+          <div class="content">
+            <Select v-model="baseAttr.textPosition" @on-change="changeCommon">
+              <Option value="bottom">bottom</Option>
+              <Option value="top">top</Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view" v-if="baseAttr.displayValue">
+        <div class="flex-item">
+          <RadioGroup
+            class="button-group"
+            v-model="baseAttr.textAlign"
+            @on-change="changeCommon"
+            type="button"
+          >
+            <Radio v-for="(item, i) in textAlignList" :label="item" :key="item">
+              <span v-html="textAlignListSvg[i]"></span>
+            </Radio>
+          </RadioGroup>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鏉$爜</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.lineColor" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item" v-if="baseAttr.displayValue">
+          <div class="content">
+            <InputNumber
+              v-model="baseAttr.fontSize"
+              @on-change="changeCommon"
+              append="瀛楀彿"
+              :min="1"
+            ></InputNumber>
+          </div>
+        </div>
+      </div>
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鑳屾櫙</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.background" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item">
+          <span class="label" style="margin-left: 10px">绫诲瀷</span>
+          <div class="content">
+            <Select v-model="baseAttr.format" @on-change="changeCommon" style="width: 90px">
+              <Option v-for="item in barcodeTypeList" :value="item" :key="item">
+                {{ item }}
+              </Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import InputNumber from '@/components/inputNumber';
+import { toRaw } from 'vue';
+
+import left from '@/assets/icon/barcode/left.svg?raw';
+import right from '@/assets/icon/barcode/right.svg?raw';
+import center from '@/assets/icon/barcode/center.svg?raw';
+
+const update = getCurrentInstance();
+const { isOne, canvasEditor, isMatchType } = useSelect(['image']);
+
+// 鏂囧瓧鍏冪礌
+const extensionType = ref('');
+
+const isBarcode = computed(() => extensionType.value === 'barcode');
+
+// 灞炴�у��
+const baseAttr = reactive({
+  value: '',
+  format: '',
+  text: '12121',
+  textAlign: 'left',
+  textPosition: 'bottom',
+  fontSize: 12,
+  background: '',
+  lineColor: '',
+  displayValue: false,
+});
+
+// 瀛椾綋瀵归綈鏂瑰紡
+const textAlignList = ['left', 'center', 'right'];
+// 瀵归綈鍥炬爣
+const textAlignListSvg = [left, center, right];
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  extensionType.value = activeObject?.extensionType || '';
+  if (activeObject && isMatchType && activeObject?.extensionType === 'barcode') {
+    baseAttr.value = activeObject.get('extension').value;
+    baseAttr.format = activeObject.get('extension').format;
+    baseAttr.text = activeObject.get('extension').text;
+    baseAttr.textAlign = activeObject.get('extension').textAlign;
+    baseAttr.textPosition = activeObject.get('extension').textPosition;
+    baseAttr.fontSize = activeObject.get('extension').fontSize;
+    baseAttr.background = activeObject.get('extension').background;
+    baseAttr.lineColor = activeObject.get('extension').lineColor;
+    baseAttr.displayValue = activeObject.get('extension').displayValue;
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = () => {
+  canvasEditor.setBarcode(toRaw(baseAttr));
+  canvasEditor.canvas.renderAll();
+};
+
+const selectCancel = () => {
+  extensionType.value = '';
+  update?.proxy?.$forceUpdate();
+};
+
+const barcodeTypeList = ref([]);
+barcodeTypeList.value = canvasEditor.getBarcodeTypes();
+
+onMounted(() => {
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+.font-selector {
+  :deep(.ivu-select-item) {
+    padding: 1px 4px;
+  }
+
+  .font-item {
+    height: 40px;
+    width: 330px;
+    background-size: auto 40px;
+    background-repeat: no-repeat;
+  }
+}
+
+.flex-view {
+  width: 100%;
+  margin-bottom: 5px;
+  padding: 5px;
+  display: inline-flex;
+  justify-content: space-between;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+.flex-item {
+  display: inline-flex;
+  flex: 1;
+  .label {
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    display: inline-block;
+    font-size: 14px;
+    // color: #333333;
+  }
+  .content {
+    flex: 1;
+    align-content: center;
+    // width: 60px;
+  }
+  .slider-box {
+    width: calc(100% - 50px);
+    margin-left: 10px;
+  }
+  .left {
+    flex: 1;
+  }
+  .right {
+    flex: 1;
+    margin-left: 10px;
+    :deep(.ivu-input-number) {
+      display: block;
+      width: 100%;
+    }
+  }
+  :deep(.ivu-slider-wrap) {
+    margin: 13px 0;
+  }
+  :deep(.ivu-radio-group-button) {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    & .ivu-radio-wrapper {
+      // width: 48px;
+      flex: 1;
+      line-height: 40px;
+      text-align: center;
+      svg {
+        vertical-align: baseline;
+      }
+    }
+  }
+
+  :deep(.ivu-btn-group) {
+    display: flex;
+    flex: 1;
+    .ivu-btn {
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-btn-group-large) {
+    & > .ivu-btn {
+      font-size: 24px;
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-radio-group-button) {
+    &.ivu-radio-group-large .ivu-radio-wrapper {
+      font-size: 24px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeBorder.vue b/src/fabric-editor/components/attributeBorder.vue
new file mode 100644
index 0000000..e971613
--- /dev/null
+++ b/src/fabric-editor/components/attributeBorder.vue
@@ -0,0 +1,245 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 10:18:57
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:31:43
+ * @Description: 杈规
+-->
+<template>
+  <div class="box attr-item-box" v-if="isOne && !isGroup">
+    <!-- <h3>杈规</h3> -->
+    <Divider plain orientation="left"><h4>杈规</h4></Divider>
+    <!-- 閫氱敤灞炴�� -->
+    <div>
+      <Row :gutter="12">
+        <Col flex="1">
+          <div class="ivu-col__box">
+            <span class="label">{{ $t('color') }}</span>
+            <div class="content">
+              <ColorPicker
+                v-model="baseAttr.stroke"
+                @on-change="(value) => changeCommon('stroke', value)"
+                alpha
+              />
+            </div>
+          </div>
+        </Col>
+        <Col flex="1">
+          <InputNumber
+            v-model="baseAttr.strokeWidth"
+            @on-change="(value) => changeCommon('strokeWidth', value)"
+            :append="$t('width')"
+            :min="0"
+          ></InputNumber>
+        </Col>
+      </Row>
+
+      <Row :gutter="12">
+        <Col flex="1">
+          <div class="ivu-col__box">
+            <span class="label">{{ $t('attributes.stroke') }}</span>
+            <div class="content">
+              <Select v-model="baseAttr.strokeDashArray" @on-change="borderSet">
+                <Option
+                  v-for="item in strokeDashList"
+                  :value="item.label"
+                  :key="`stroke-${item.label}`"
+                >
+                  {{ item.label }}
+                </Option>
+              </Select>
+            </div>
+          </div>
+        </Col>
+      </Row>
+    </div>
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import InputNumber from '@/components/inputNumber';
+
+const update = getCurrentInstance();
+const { isOne, isGroup, canvasEditor } = useSelect();
+
+const groupType = ['group'];
+// 灞炴�у��
+const baseAttr = reactive({
+  stroke: '#fff',
+  strokeWidth: 0,
+  strokeDashArray: [],
+});
+
+const strokeDashList = [
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [],
+      strokeLineCap: 'butt',
+    },
+    label: 'Stroke',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [1, 10],
+      strokeLineCap: 'butt',
+    },
+    label: 'Dash-1',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [1, 10],
+      strokeLineCap: 'round',
+    },
+    label: 'Dash-2',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [15, 15],
+      strokeLineCap: 'square',
+    },
+    label: 'Dash-3',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [15, 15],
+      strokeLineCap: 'round',
+    },
+    label: 'Dash-4',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [25, 25],
+      strokeLineCap: 'square',
+    },
+    label: 'Dash-5',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [25, 25],
+      strokeLineCap: 'round',
+    },
+    label: 'Dash-6',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [1, 8, 16, 8, 1, 20],
+      strokeLineCap: 'square',
+    },
+    label: 'Dash-7',
+  },
+  {
+    value: {
+      strokeUniform: true,
+      strokeDashArray: [1, 8, 16, 8, 1, 20],
+      strokeLineCap: 'round',
+    },
+    label: 'Dash-8',
+  },
+];
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && !groupType.includes(activeObject.type)) {
+    baseAttr.stroke = activeObject.get('stroke');
+    baseAttr.strokeWidth = activeObject.get('strokeWidth');
+    const strokeDashArray = JSON.stringify(activeObject.get('strokeDashArray') || []);
+    const target = strokeDashList.find((item) => {
+      return (
+        JSON.stringify(item.value.strokeDashArray) === strokeDashArray &&
+        activeObject.get('strokeLineCap') === item.value.strokeLineCap
+      );
+    });
+    if (target) {
+      baseAttr.strokeDashArray = target.label;
+    }
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject.set(key, value);
+    activeObject.set('strokeUniform', true);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+// 杈规璁剧疆
+const borderSet = (key) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    const stroke = strokeDashList.find((item) => item.label === key);
+    activeObject.set(stroke.value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeColor.vue b/src/fabric-editor/components/attributeColor.vue
new file mode 100644
index 0000000..269fb66
--- /dev/null
+++ b/src/fabric-editor/components/attributeColor.vue
@@ -0,0 +1,161 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 10:59:48
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:32:19
+ * @Description: 娓愬彉
+-->
+
+<template>
+  <div class="box attr-item-box" v-if="isOne && selectType !== 'image' && selectType !== 'group'">
+    <Divider plain orientation="left"><h4>棰滆壊</h4></Divider>
+    <!-- 閫氱敤灞炴�� -->
+    <div class="bg-item">
+      <Tooltip placement="top" theme="light">
+        <div class="color-bar" :style="{ background: baseAttr.fill }"></div>
+        <template #content>
+          <color-picker
+            v-model:value="baseAttr.fill"
+            :modes="['娓愬彉', '绾壊']"
+            @change="colorChange"
+            @nativePick="dropColor"
+          ></color-picker>
+        </template>
+      </Tooltip>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import colorPicker from './color-picker';
+import { toRaw } from 'vue';
+
+const update = getCurrentInstance();
+const { fabric, selectType, canvasEditor, isOne } = useSelect();
+const angleKey = 'gradientAngle';
+// 灞炴�у��
+const baseAttr = reactive({
+  fill: '#ffffffff',
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && isOne) {
+    const fill = activeObject.get('fill');
+    if (typeof fill === 'string') {
+      baseAttr.fill = fill;
+    } else {
+      baseAttr.fill = fabricGradientToCss(fill, activeObject);
+    }
+  }
+};
+
+const colorChange = (value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    const color = String(value.color).replace('NaN', '');
+    if (value.mode === '绾壊') {
+      activeObject.set('fill', color);
+    } else if (value.mode === '娓愬彉') {
+      const currentGradient = cssToFabricGradient(
+        toRaw(value.stops),
+        activeObject.width,
+        activeObject.height,
+        value.angle
+      );
+      activeObject.set('fill', currentGradient, value.angle);
+      activeObject.set(angleKey, value.angle);
+    }
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const dropColor = (value) => {
+  colorChange(value);
+};
+
+const fabricGradientToCss = (val, activeObject) => {
+  // 娓愬彉绫诲瀷
+  if (!val) return;
+  const angle = activeObject.get(angleKey, val.degree);
+  const colorStops = val.colorStops.map((item) => {
+    return item.color + ' ' + item.offset * 100 + '%';
+  });
+  return `linear-gradient(${angle}deg, ${colorStops})`;
+};
+// css杞現abric娓愬彉
+const cssToFabricGradient = (stops, width, height, angle) => {
+  const gradAngleToCoords = (paramsAngle) => {
+    const anglePI = -parseInt(paramsAngle, 10) * (Math.PI / 180);
+    return {
+      x1: Math.round(50 + Math.sin(anglePI) * 50) / 100,
+      y1: Math.round(50 + Math.cos(anglePI) * 50) / 100,
+      x2: Math.round(50 + Math.sin(anglePI + Math.PI) * 50) / 100,
+      y2: Math.round(50 + Math.cos(anglePI + Math.PI) * 50) / 100,
+    };
+  };
+
+  const angleCoords = gradAngleToCoords(angle);
+  return new fabric.Gradient({
+    type: 'linear',
+    gradientUnits: 'pencentage', // pixels or pencentage 鍍忕礌 鎴栬�� 鐧惧垎姣�
+    coords: {
+      x1: angleCoords.x1 * width,
+      y1: angleCoords.y1 * height,
+      x2: angleCoords.x2 * width,
+      y2: angleCoords.y2 * height,
+    },
+    colorStops: [...stops],
+  });
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+.color-bar {
+  // width: 30px;
+  height: 30px;
+  cursor: pointer;
+  border: 2px solid #f6f7f9;
+}
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-tooltip) {
+  display: flex;
+}
+.ivu-form-item {
+  background: #f6f7f9;
+  border-radius: 5px;
+  padding: 0 5px;
+  margin-bottom: 10px;
+}
+
+.ivu-row {
+  margin-bottom: 10px;
+}
+</style>
diff --git a/src/fabric-editor/components/attributeFont.vue b/src/fabric-editor/components/attributeFont.vue
new file mode 100644
index 0000000..95c66f1
--- /dev/null
+++ b/src/fabric-editor/components/attributeFont.vue
@@ -0,0 +1,426 @@
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType">
+    <!-- <h3>瀛椾綋灞炴��</h3> -->
+    <el-divider content-position="left"><h4>瀛椾綋灞炴��</h4></el-divider>
+    <div>
+      <!-- <Divider plain orientation="left">{{ $t('attributes.font') }}</Divider> -->
+      <div class="flex-view">
+        <div class="flex-item">
+          <!-- <div class="left font-selector">
+            <Select v-model="baseAttr.fontFamily" @on-change="changeFontFamily">
+              <Option v-for="item in fontsList" :value="item.name" :key="`font-${item.name}`">
+                <div class="font-item" :style="`background-image:url('${item.img}');`">
+                  {{ !item.img ? item : '' }}
+                  <span style="display: none">{{ item.name }}</span>
+                </div>
+              </Option>
+            </Select>
+          </div> -->
+          <div class="right2">
+            <el-input-number
+              v-model="baseAttr.fontSize"
+              @change="(value) => changeCommon('fontSize', value)"
+              :min="1"
+              controls-position="right"
+            >
+              <template #prefix>瀛楀彿</template>
+            </el-input-number>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <el-radio-group
+            class="button-group"
+            v-model="baseAttr.textAlign"
+            @change="(value) => changeCommon('textAlign', value)"
+          >
+            <el-radio-button v-for="(item, i) in textAlignList" :value="item" :key="item">
+              <span v-html="textAlignListSvg[i]"></span>
+            </el-radio-button>
+          </el-radio-group>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <el-radio-group class="button-group">
+            <el-button @click="changeFontWeight('fontWeight', baseAttr.fontWeight)">
+              <fontWeight
+                :fill="baseAttr.fontWeight === 'bold' ? '#305ef4' : '#666'"
+                width="14"
+                height="14"
+              ></fontWeight>
+            </el-button>
+            <el-button @click="changeFontStyle('fontStyle', baseAttr.fontStyle)">
+              <fontStyle
+                :fill="baseAttr.fontStyle === 'italic' ? '#305ef4' : '#666'"
+                width="14"
+                height="14"
+              ></fontStyle>
+            </el-button>
+            <el-button @click="changeLineThrough('linethrough', baseAttr.linethrough)">
+              <linethrough
+                :fill="baseAttr.linethrough ? '#305ef4' : '#666'"
+                width="14"
+                height="14"
+              ></linethrough>
+            </el-button>
+            <el-button @click="changeUnderline('underline', baseAttr.underline)">
+              <underline
+                :fill="baseAttr.underline ? '#305ef4' : '#666'"
+                width="14"
+                height="14"
+              ></underline>
+            </el-button>
+          </el-radio-group>
+        </div>
+      </div>
+
+      <el-row class="flex-view">
+        <el-col :span="12">
+          <el-input-number
+            v-model="baseAttr.lineHeight"
+            @change="(value) => changeCommon('lineHeight', value)"
+            :step="0.1"
+            controls-position="right"
+            style="width: 100%"
+          >
+            <template #prefix>琛岄珮</template>
+          </el-input-number>
+        </el-col>
+        <el-col :span="12">
+          <el-input-number
+            v-model="baseAttr.charSpacing"
+            @change="(value) => changeCommon('charSpacing', value)"
+            controls-position="right"
+            style="margin-left: 10px; width: 100%"
+          >
+            <template #prefix>闂磋窛</template>
+          </el-input-number>
+        </el-col>
+      </el-row>
+
+      <!-- <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">{{ $t('background') }}</span>
+          <div class="content">
+            <ColorPicker
+              v-model="baseAttr.textBackgroundColor"
+              @on-change="(value) => changeCommon('textBackgroundColor', value)"
+              alpha
+            />
+          </div>
+        </div>
+      </div> -->
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/fabric-editor/hooks/select';
+// import InputNumber from '@/components/inputNumber';
+import fontWeight from '@/fabric-editor/assets/icon/attribute/fontWeight.svg?component';
+import fontStyle from '@/fabric-editor/assets/icon/attribute/fontStyle.svg?component';
+import linethrough from '@/fabric-editor/assets/icon/attribute/linethrough.svg?component';
+import underline from '@/fabric-editor/assets/icon/attribute/underline.svg?component';
+
+import textAlignLeft from '@/fabric-editor/assets/icon/attribute/textAlignLeft.svg?raw';
+import textAlignRight from '@/fabric-editor/assets/icon/attribute/textAlignRight.svg?raw';
+import textAlignCenter from '@/fabric-editor/assets/icon/attribute/textAlignCenter.svg?raw';
+import textAlignJustitfy from '@/fabric-editor/assets/icon/attribute/textAlignJustitfy.svg?raw';
+import { ElLoading } from 'element-plus';
+import { TemplateParamObjectName } from '@/fabric-editor/customObject';
+
+const update = getCurrentInstance();
+
+// 鏂囧瓧鍏冪礌
+const textType = ['i-text', 'textbox', 'text', TemplateParamObjectName];
+const { canvasEditor, isMatchType, isOne } = useSelect(textType);
+
+// 灞炴�у��
+const baseAttr = reactive({
+  fontSize: 0,
+  fontFamily: '',
+  lineHeight: 0,
+  charSpacing: 0,
+  fontWeight: '',
+  textBackgroundColor: '#fff',
+  textAlign: '',
+  fontStyle: '',
+  underline: false,
+  linethrough: false,
+  overline: false,
+});
+
+const fontsList = ref([]);
+canvasEditor.getFontList().then((list) => {
+  fontsList.value = list;
+});
+
+// 瀛椾綋瀵归綈鏂瑰紡
+const textAlignList = ['left', 'center', 'right', 'justify'];
+// 瀵归綈鍥炬爣
+const textAlignListSvg = [textAlignLeft, textAlignCenter, textAlignRight, textAlignJustitfy];
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && isMatchType) {
+    baseAttr.fontSize = activeObject.get('fontSize');
+    baseAttr.fontFamily = activeObject.get('fontFamily');
+    baseAttr.lineHeight = activeObject.get('lineHeight');
+    baseAttr.textAlign = activeObject.get('textAlign');
+    baseAttr.underline = activeObject.get('underline');
+    baseAttr.linethrough = activeObject.get('linethrough');
+    baseAttr.charSpacing = activeObject.get('charSpacing');
+    baseAttr.overline = activeObject.get('overline');
+    baseAttr.fontStyle = activeObject.get('fontStyle');
+    baseAttr.textBackgroundColor = activeObject.get('textBackgroundColor');
+    baseAttr.fontWeight = activeObject.get('fontWeight');
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject && activeObject.set(key, value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+//@ts-ignore
+let loadingInstance;
+
+const changeFontFamily = async (fontName) => {
+  if (!fontName) return;
+  loadingInstance = ElLoading.service({
+    fullscreen: true,
+    lock: false,
+    background: 'transparent',
+  });
+  canvasEditor.loadFont(fontName).finally(() => {
+    if (loadingInstance) {
+      loadingInstance.close();
+    }
+  });
+};
+const changeFontWeight = (key, value) => {
+  const nValue = value === 'normal' ? 'bold' : 'normal';
+  baseAttr.fontWeight = nValue;
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  activeObject && activeObject.set(key, nValue);
+  canvasEditor.canvas.renderAll();
+};
+
+// 鏂滀綋
+const changeFontStyle = (key, value) => {
+  const nValue = value === 'normal' ? 'italic' : 'normal';
+  baseAttr.fontStyle = nValue;
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  activeObject && activeObject.set(key, nValue);
+  canvasEditor.canvas.renderAll();
+};
+
+// 涓垝
+const changeLineThrough = (key, value) => {
+  const nValue = value === false;
+  baseAttr.linethrough = nValue;
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  activeObject && activeObject.set(key, nValue);
+  canvasEditor.canvas.renderAll();
+};
+
+// 涓嬪垝
+const changeUnderline = (key, value) => {
+  const nValue = value === false;
+  baseAttr.underline = nValue;
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  activeObject && activeObject.set(key, nValue);
+  canvasEditor.canvas.renderAll();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="scss">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+
+.ivu-row {
+  margin-bottom: 8px;
+
+  .ivu-col {
+    position: inherit;
+
+    &__box {
+      display: flex;
+      align-items: center;
+      border-radius: 4px;
+      background: #f8f8f8;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+
+  .content {
+    flex: 1;
+
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      border: none !important;
+      background-color: transparent;
+      box-shadow: none !important;
+    }
+  }
+}
+
+.font-selector {
+  :deep(.ivu-select-item) {
+    padding: 1px 4px;
+  }
+
+  .font-item {
+    width: 280px;
+    height: 40px;
+    background-repeat: no-repeat;
+    background-size: auto 28px;
+  }
+}
+
+.flex-view {
+  display: inline-flex;
+  justify-content: space-between;
+  margin-bottom: 5px;
+  padding: 5px;
+  width: 100%;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+
+.flex-item {
+  display: inline-flex;
+  flex: 1;
+
+  .label {
+    display: inline-block;
+    width: 32px;
+    height: 32px;
+    font-size: 14px;
+    line-height: 32px;
+    // color: #333333;
+  }
+
+  .content {
+    flex: 1;
+    // width: 60px;
+  }
+
+  .slider-box {
+    margin-left: 10px;
+    width: calc(100% - 50px);
+  }
+
+  .left {
+    flex: 1;
+  }
+
+  .right {
+    flex: 1;
+    margin-left: 10px;
+
+    :deep(.ivu-input-number) {
+      display: block;
+      width: 100%;
+    }
+  }
+
+  :deep(.ivu-slider-wrap) {
+    margin: 13px 0;
+  }
+
+  :deep(.ivu-radio-group-button, ) {
+    display: flex;
+    flex: 1;
+    width: 100%;
+
+    & .ivu-radio-wrapper {
+      // width: 48px;
+      flex: 1;
+      line-height: 40px;
+      text-align: center;
+
+      svg {
+        vertical-align: baseline;
+      }
+    }
+  }
+
+  :deep() {
+    .el-radio-group {
+      width: 100%;
+
+      .el-radio-button {
+        flex: 1;
+        min-width: 0;
+
+        .el-radio-button__inner {
+          width: 100%;
+        }
+      }
+    }
+  }
+
+  :deep(.ivu-btn-group) {
+    display: flex;
+    flex: 1;
+
+    .ivu-btn {
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-btn-group-large) {
+    & > .ivu-btn {
+      font-size: 24px;
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-radio-group-button) {
+    &.ivu-radio-group-large .ivu-radio-wrapper {
+      font-size: 24px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeId.vue b/src/fabric-editor/components/attributeId.vue
new file mode 100644
index 0000000..fd55882
--- /dev/null
+++ b/src/fabric-editor/components/attributeId.vue
@@ -0,0 +1,112 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 09:53:33
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:33:27
+ * @Description: file content
+-->
+
+<template>
+  <div class="box attr-item-box" v-if="isOne">
+    <!-- <h3>鏁版嵁</h3> -->
+    <Divider plain orientation="left"><h4>鏁版嵁</h4></Divider>
+
+    <Form :label-width="40" class="form-wrap">
+      <FormItem :label="$t('attributes.id')">
+        <Input
+          v-model="baseAttr.id"
+          @on-change="changeCommon('id', baseAttr.id)"
+          size="small"
+        ></Input>
+      </FormItem>
+    </Form>
+
+    <Row :gutter="10">
+      <Col flex="1">
+        <Select
+          v-model="baseAttr.linkData[0]"
+          filterable
+          allow-create
+          @on-change="changeCommon('linkData', baseAttr.linkData)"
+        >
+          <Option value="src"></Option>
+          <Option value="text"></Option>
+        </Select>
+      </Col>
+      <Col flex="1">
+        <Input v-model="baseAttr.linkData[1]" placeholder="璇疯緭鍏�" />
+      </Col>
+    </Row>
+
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+
+const update = getCurrentInstance();
+const { canvasEditor, isOne } = useSelect();
+
+// 灞炴�у��
+const baseAttr = reactive({
+  id: 0,
+  linkData: ['', ''],
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject) {
+    baseAttr.id = activeObject.get('id');
+    baseAttr.linkData = activeObject.get('linkData') || ['', ''];
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject && activeObject.set(key, value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+.ivu-form-item {
+  background: #f6f7f9;
+  border-radius: 5px;
+  padding: 0 5px;
+  margin-bottom: 10px;
+}
+
+.ivu-row {
+  margin-bottom: 10px;
+}
+</style>
diff --git a/src/fabric-editor/components/attributePostion.vue b/src/fabric-editor/components/attributePostion.vue
new file mode 100644
index 0000000..577e1dd
--- /dev/null
+++ b/src/fabric-editor/components/attributePostion.vue
@@ -0,0 +1,172 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 09:23:36
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:33:41
+ * @Description: file content
+-->
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType">
+    <!-- <h3>浣嶇疆淇℃伅</h3> -->
+    <el-divider content-position="left"><h4>浣嶇疆淇℃伅</h4></el-divider>
+    <!-- 閫氱敤灞炴�� -->
+    <div v-show="isMatchType">
+      <el-row class="flex-view">
+        <el-col :span="12">
+          <el-input-number
+            v-model="baseAttr.left"
+            @change="(value) => changeCommon('left', value)"
+            controls-position="right"
+            style="width: 100%"
+            :precision="2"
+          >
+            <template #prefix>X杞�</template>
+          </el-input-number>
+        </el-col>
+        <el-col :span="12">
+          <el-input-number
+            v-model="baseAttr.top"
+            @change="(value) => changeCommon('top', value)"
+            controls-position="right"
+            style="margin-left: 10px; width: 100%"
+            :precision="2"
+          >
+            <template #prefix>Y杞�</template>
+          </el-input-number>
+        </el-col>
+      </el-row>
+      <el-form :label-width="50" class="form-wrap" label-position="left">
+        <el-form-item :label="'瀵归綈'">
+          <el-slider
+            v-model="baseAttr.angle"
+            :max="360"
+            @input="(value) => changeCommon('angle', value)"
+          ></el-slider>
+        </el-form-item>
+        <el-form-item :label="'閫忔槑'">
+          <el-slider
+            v-model="baseAttr.opacity"
+            @input="(value) => changeCommon('opacity', value)"
+          ></el-slider>
+        </el-form-item>
+      </el-form>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/fabric-editor/hooks/select';
+// import InputNumber from '@/components/inputNumber';
+import { TemplateParamObjectName } from '@/fabric-editor/customObject';
+
+const update = getCurrentInstance();
+
+// 鍙慨鏀圭殑鍏冪礌
+const baseType = [
+  'text',
+  'i-text',
+  'textbox',
+  'rect',
+  'circle',
+  'triangle',
+  'polygon',
+  // 'image',
+  'group',
+  'line',
+  'arrow',
+  'thinTailArrow',
+  TemplateParamObjectName,
+];
+const { isMatchType, canvasEditor, isOne } = useSelect(baseType);
+
+// 灞炴�у��
+const baseAttr = reactive({
+  opacity: 0,
+  angle: 0,
+  left: 0,
+  top: 0,
+  rx: 0,
+  ry: 0,
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && isMatchType) {
+    baseAttr.opacity = activeObject.get('opacity') * 100;
+    baseAttr.left = activeObject.get('left');
+    baseAttr.top = activeObject.get('top');
+    baseAttr.angle = activeObject.get('angle') || 0;
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    // 閫忔槑搴︾壒娈婅浆鎹�
+    if (key === 'opacity') {
+      activeObject && activeObject.set(key, value / 100);
+      canvasEditor.canvas.renderAll();
+      return;
+    }
+    // 鏃嬭浆瑙掑害閫傞厤
+    if (key === 'angle') {
+      activeObject.rotate(value);
+      canvasEditor.canvas.renderAll();
+      return;
+    }
+    activeObject && activeObject.set(key, value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="scss">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+.ivu-form-item {
+  margin-bottom: 10px;
+  padding: 0 5px;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+
+.ivu-row {
+  margin-bottom: 10px;
+}
+
+.flex-view {
+  display: inline-flex;
+  justify-content: space-between;
+  margin-bottom: 5px;
+  padding: 5px;
+  width: 100%;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+</style>
diff --git a/src/fabric-editor/components/attributeQrCode.vue b/src/fabric-editor/components/attributeQrCode.vue
new file mode 100644
index 0000000..0efc6f4
--- /dev/null
+++ b/src/fabric-editor/components/attributeQrCode.vue
@@ -0,0 +1,335 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-06 20:04:48
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:34:37
+ * @Description: 浜岀淮鐮佺粍浠�
+-->
+
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType && isQrcode">
+    <!-- <h3>瀛椾綋灞炴��</h3> -->
+    <Divider plain orientation="left"><h4>浜屼綅鐮佸睘鎬�</h4></Divider>
+    <div>
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鍐呭</span>
+          <div class="content">
+            <Input v-model="baseAttr.data" @on-change="changeCommon" />
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <div class="content">
+            <InputNumber
+              v-model="baseAttr.width"
+              @on-change="changeCommon"
+              append="瀹藉害"
+              :min="1"
+            ></InputNumber>
+          </div>
+        </div>
+        <div class="flex-item">
+          <div class="content">
+            <InputNumber
+              v-model="baseAttr.margin"
+              @on-change="changeCommon"
+              append="杈硅窛"
+              :min="1"
+            ></InputNumber>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鏁g偣</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.dotsColor" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item">
+          <span class="label" style="margin-left: 10px">绫诲瀷</span>
+          <div class="content">
+            <Select v-model="baseAttr.dotsType" @on-change="changeCommon" style="width: 90px">
+              <Option v-for="item in optionsList.DotsType" :value="item" :key="item">
+                {{ item }}
+              </Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">澶栬</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.cornersSquareColor" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item">
+          <span class="label" style="margin-left: 10px">绫诲瀷</span>
+          <div class="content">
+            <Select
+              v-model="baseAttr.cornersSquareType"
+              @on-change="changeCommon"
+              style="width: 90px"
+            >
+              <Option v-for="item in optionsList.cornersDotType" :value="item" :key="item">
+                {{ item }}
+              </Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鍐呰</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.cornersDotColor" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item">
+          <span class="label" style="margin-left: 10px">绫诲瀷</span>
+          <div class="content">
+            <Select v-model="baseAttr.cornersDotType" @on-change="changeCommon" style="width: 90px">
+              <Option v-for="item in optionsList.cornersDotType" :value="item" :key="item">
+                {{ item }}
+              </Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex-view">
+        <div class="flex-item">
+          <span class="label">鑳屾櫙</span>
+          <div class="content">
+            <ColorPicker v-model="baseAttr.background" @on-change="changeCommon" alpha />
+          </div>
+        </div>
+        <div class="flex-item">
+          <span class="label" style="margin-left: 10px">瀹归敊</span>
+          <div class="content">
+            <Select
+              v-model="baseAttr.errorCorrectionLevel"
+              @on-change="changeCommon"
+              style="width: 90px"
+            >
+              <Option
+                v-for="item in optionsList.errorCorrectionLevelType"
+                :value="item"
+                :key="item"
+              >
+                {{ item }}
+              </Option>
+            </Select>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import InputNumber from '@/components/inputNumber';
+import { toRaw } from 'vue';
+
+const update = getCurrentInstance();
+const { canvasEditor, isOne, isMatchType } = useSelect(['image']);
+
+// 鏂囧瓧鍏冪礌
+const extensionType = ref('');
+
+const isQrcode = computed(() => extensionType.value === 'qrcode');
+
+// 灞炴�у��
+const baseAttr = reactive({
+  data: '',
+  width: 300,
+  margin: 10,
+  errorCorrectionLevel: 'M',
+  dotsColor: 'red',
+  dotsType: 'rounded',
+  cornersSquareColor: 'black',
+  cornersSquareType: 'dot',
+  cornersDotColor: 'black',
+  cornersDotType: 'square',
+  background: '#ffffff',
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  extensionType.value = activeObject?.extensionType || '';
+  if (activeObject && isMatchType && activeObject?.extensionType === 'qrcode') {
+    const extension = activeObject.get('extension');
+    Object.keys(extension).forEach((key) => {
+      baseAttr[key] = extension[key];
+    });
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = () => {
+  canvasEditor.setQrCode(toRaw(baseAttr));
+  canvasEditor.canvas.renderAll();
+};
+
+const selectCancel = () => {
+  extensionType.value = '';
+  update?.proxy?.$forceUpdate();
+};
+
+// 瀹归敊鐜�
+
+const res = canvasEditor.getQrCodeTypes();
+const optionsList = reactive(res);
+
+onMounted(() => {
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+.font-selector {
+  :deep(.ivu-select-item) {
+    padding: 1px 4px;
+  }
+
+  .font-item {
+    height: 40px;
+    width: 330px;
+    background-size: auto 40px;
+    background-repeat: no-repeat;
+  }
+}
+
+.flex-view {
+  width: 100%;
+  margin-bottom: 5px;
+  padding: 5px;
+  display: inline-flex;
+  justify-content: space-between;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+.flex-item {
+  display: inline-flex;
+  flex: 1;
+  .label {
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    display: inline-block;
+    font-size: 14px;
+    // color: #333333;
+  }
+  .content {
+    flex: 1;
+    // width: 60px;
+  }
+  .slider-box {
+    width: calc(100% - 50px);
+    margin-left: 10px;
+  }
+  .left {
+    flex: 1;
+  }
+  .right {
+    flex: 1;
+    margin-left: 10px;
+    :deep(.ivu-input-number) {
+      display: block;
+      width: 100%;
+    }
+  }
+  :deep(.ivu-slider-wrap) {
+    margin: 13px 0;
+  }
+  :deep(.ivu-radio-group-button) {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    & .ivu-radio-wrapper {
+      // width: 48px;
+      flex: 1;
+      line-height: 40px;
+      text-align: center;
+      svg {
+        vertical-align: baseline;
+      }
+    }
+  }
+
+  :deep(.ivu-btn-group) {
+    display: flex;
+    flex: 1;
+    .ivu-btn {
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-btn-group-large) {
+    & > .ivu-btn {
+      font-size: 24px;
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-radio-group-button) {
+    &.ivu-radio-group-large .ivu-radio-wrapper {
+      font-size: 24px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeRounded.vue b/src/fabric-editor/components/attributeRounded.vue
new file mode 100644
index 0000000..c51d671
--- /dev/null
+++ b/src/fabric-editor/components/attributeRounded.vue
@@ -0,0 +1,126 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 10:18:57
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:35:31
+ * @Description: 鍦嗚
+-->
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType">
+    <!-- <h3>鍦嗚</h3> -->
+    <Divider plain orientation="left"><h4>鍦嗚</h4></Divider>
+    <!-- 閫氱敤灞炴�� -->
+    <div>
+      <Row :gutter="10">
+        <Col :span="18" flex="1">
+          <Form :label-width="40" class="form-wrap">
+            <FormItem :label="$t('attributes.rx_ry')">
+              <Slider
+                v-model="baseAttr.roundValue"
+                :max="300"
+                @on-input="(value) => changeCommon(value)"
+              ></Slider>
+            </FormItem>
+          </Form>
+        </Col>
+        <Col :span="6" flex="1">
+          <InputNumber
+            v-model="baseAttr.roundValue"
+            :min="0"
+            :max="300"
+            @on-change="(value) => changeCommon(value)"
+          ></InputNumber>
+        </Col>
+      </Row>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+
+const update = getCurrentInstance();
+const { canvasEditor, isOne, isMatchType } = useSelect(['rect']);
+
+// 灞炴�у��
+const baseAttr = reactive({
+  roundValue: 0,
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject) {
+    baseAttr.roundValue = activeObject.get('roundValue');
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject.set('ry', value);
+    activeObject.set('rx', value);
+    activeObject.set('roundValue', value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇鍦嗚鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeShadow.vue b/src/fabric-editor/components/attributeShadow.vue
new file mode 100644
index 0000000..072cb5f
--- /dev/null
+++ b/src/fabric-editor/components/attributeShadow.vue
@@ -0,0 +1,148 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-21 10:10:24
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:18:47
+ * @Description: 闃村奖
+-->
+
+<template>
+  <div class="box attr-item-box" v-if="isOne">
+    <!-- <h3>闃村奖</h3> -->
+    <Divider plain orientation="left"><h4>闃村奖</h4></Divider>
+    <!-- 閫氱敤灞炴�� -->
+    <div>
+      <Row :gutter="10">
+        <Col flex="1">
+          <div class="ivu-col__box">
+            <span class="label">{{ $t('color') }}</span>
+            <div class="content">
+              <ColorPicker v-model="baseAttr.shadow.color" @on-change="changeCommon" alpha />
+            </div>
+          </div>
+        </Col>
+        <Col flex="1">
+          <InputNumber
+            v-model="baseAttr.shadow.blur"
+            :defaultValue="0"
+            @on-change="changeCommon"
+            :append="$t('attributes.blur')"
+            :min="0"
+          ></InputNumber>
+        </Col>
+      </Row>
+      <div>
+        <Row :gutter="10">
+          <Col flex="1">
+            <InputNumber
+              v-model="baseAttr.shadow.offsetX"
+              :defaultValue="0"
+              @on-change="changeCommon"
+              :append="$t('attributes.offset_x')"
+            ></InputNumber>
+          </Col>
+          <Col flex="1">
+            <InputNumber
+              v-model="baseAttr.shadow.offsetY"
+              :defaultValue="0"
+              @on-change="changeCommon"
+              :append="$t('attributes.offset_y')"
+            ></InputNumber>
+          </Col>
+        </Row>
+      </div>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import InputNumber from '@/components/inputNumber';
+
+const update = getCurrentInstance();
+const { fabric, isOne, canvasEditor } = useSelect();
+
+// 灞炴�у��
+const baseAttr = reactive({
+  shadow: {},
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject) {
+    baseAttr.shadow = activeObject.get('shadow') || {};
+  }
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject.set('shadow', new fabric.Shadow(baseAttr.shadow));
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.box {
+  width: 100%;
+}
+
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeTemplateParam.vue b/src/fabric-editor/components/attributeTemplateParam.vue
new file mode 100644
index 0000000..415c20d
--- /dev/null
+++ b/src/fabric-editor/components/attributeTemplateParam.vue
@@ -0,0 +1,134 @@
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType">
+    <el-divider content-position="left"><h4>妯℃澘鍙傛暟灞炴��</h4></el-divider>
+    <el-form :label-width="140" class="form-wrap" label-position="left">
+      <el-form-item :label="'鏁版嵁鍙傛暟鍚嶇О'" required>
+        <el-input
+          v-model="baseAttr.label"
+          placeholder="璇疯緭鍏ユ暟鎹弬鏁板悕绉�"
+          @input="(value) => changeCommon('label', value)"
+          disabled
+        ></el-input>
+      </el-form-item>
+      <el-form-item :label="'鏁版嵁鍙傛暟瀛楁'" required>
+        <el-input
+          v-model="baseAttr.name"
+          placeholder="璇疯緭鍏ユ暟鎹弬鏁板瓧娈�"
+          @input="(value) => changeCommon('name', value)"
+          disabled
+        ></el-input>
+      </el-form-item>
+      <el-form-item :label="'鍚堝悓妯℃澘鍙橀噺绫诲瀷'" required>
+        <FieldSelect
+          v-model="baseAttr.templateParamType"
+          placeholder="璇烽�夋嫨鍚堝悓妯℃澘鍙橀噺绫诲瀷"
+          :value-enum="EnumContractTemplateValueTypeText"
+          @change="(value) => changeCommon('templateParamType', value)"
+        />
+      </el-form-item>
+      <el-form-item :label="'鍚堝悓妯℃澘鍙橀噺'" required>
+        <FieldSelect
+          v-model="baseAttr.recorder"
+          placeholder="璇烽�夋嫨鍚堝悓妯℃澘鍙橀噺"
+          :value-enum="EnumContractTemplateValueRecorderText"
+          @change="(value) => changeCommon('recorder', value)"
+        />
+      </el-form-item>
+      <el-form-item :label="'鐢ㄦ埛绫诲瀷'" required>
+        <FieldSelect
+          v-model="baseAttr.userType"
+          placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+          :value-enum="EnumUserTypeText"
+          @change="(value) => changeCommon('userType', value)"
+        />
+      </el-form-item>
+      <el-form-item :label="'鏄惁蹇呭~'" required>
+        <FieldSwitch
+          v-model="baseAttr.required"
+          @change="(value) => changeCommon('required', value)"
+        />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import useSelect from '@/fabric-editor/hooks/select';
+// import InputNumber from '@/components/inputNumber';
+import { TemplateParamObjectName } from '@/fabric-editor/customObject';
+import {
+  EnumContractTemplateValueType,
+  EnumContractTemplateValueTypeText,
+  EnumContractTemplateValueRecorderText,
+  EnumUserTypeText,
+} from '@/constants';
+import { FieldSelect, FieldSwitch } from '@bole-core/components';
+
+defineOptions({
+  name: 'attributeTemplateParam',
+});
+
+const update = getCurrentInstance();
+
+// 鍙慨鏀圭殑鍏冪礌
+const baseType = [TemplateParamObjectName];
+
+const { isMatchType, canvasEditor, isOne } = useSelect(baseType);
+
+// 灞炴�у��
+const baseAttr = reactive({
+  templateParamType: EnumContractTemplateValueType.Text,
+  recorder: EnumContractTemplateValueRecorder.Creator,
+  userType: EnumUserType.Personal,
+  label: '',
+  name: '',
+  required: true,
+});
+
+// 灞炴�ц幏鍙�
+const getObjectAttr = (e?: any) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  console.log('activeObject: ', activeObject);
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && isMatchType) {
+    baseAttr.templateParamType = activeObject.get('templateParamType');
+    baseAttr.recorder = activeObject.get('recorder');
+    baseAttr.userType = activeObject.get('userType');
+    baseAttr.label = activeObject.get('label');
+    baseAttr.name = activeObject.get('name');
+    baseAttr.required = activeObject.get('required');
+  }
+};
+
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+// 閫氱敤灞炴�ф敼鍙�
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    activeObject && activeObject.set(key, value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+</style>
diff --git a/src/fabric-editor/components/attributeTextContent.vue b/src/fabric-editor/components/attributeTextContent.vue
new file mode 100644
index 0000000..48e1065
--- /dev/null
+++ b/src/fabric-editor/components/attributeTextContent.vue
@@ -0,0 +1,147 @@
+<script setup name="AttrBute">
+import useSelect from '@/hooks/select';
+import InputNumber from '@/components/inputNumber';
+
+const update = getCurrentInstance();
+const { canvasEditor, isOne, isMatchType } = useSelect(['i-text']);
+const baseAttr = reactive({
+  text: '',
+  strokeWidth: 1,
+  stroke: '',
+  showPathAttr: false,
+});
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject) {
+    baseAttr.text = activeObject.get('text');
+    const path = activeObject.get('path');
+    if (path) {
+      baseAttr.strokeWidth = path.strokeWidth;
+      baseAttr.stroke = path.stroke;
+      baseAttr.showPathAttr = true;
+    } else {
+      baseAttr.showPathAttr = false;
+    }
+  }
+};
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    baseAttr[key] = value;
+    if (key === 'text') {
+      activeObject.set(key, value);
+    } else {
+      const path = activeObject.get('path');
+      path.set(key, value);
+    }
+    canvasEditor.canvas.renderAll();
+  }
+};
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+onMounted(() => {
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<template>
+  <div class="box attr-item-box" v-if="isOne && isMatchType">
+    <!-- <h3>鏁版嵁</h3> -->
+    <Divider plain orientation="left"><h4>鏂囨湰鍐呭</h4></Divider>
+
+    <Form :label-width="40" class="form-wrap">
+      <FormItem :label="$t('attributes.id')">
+        <Input
+          v-model="baseAttr.text"
+          @on-change="changeCommon('text', baseAttr.text)"
+          size="small"
+        ></Input>
+      </FormItem>
+    </Form>
+
+    <template v-if="baseAttr.showPathAttr">
+      <!-- <h3>鏁版嵁</h3> -->
+      <Divider plain orientation="left"><h4>鏂囨湰璺緞</h4></Divider>
+      <div>
+        <Row :gutter="12">
+          <Col flex="1">
+            <div class="ivu-col__box">
+              <span class="label">{{ $t('color') }}</span>
+              <div class="content">
+                <ColorPicker
+                  v-model="baseAttr.stroke"
+                  @on-change="(value) => changeCommon('stroke', value)"
+                  alpha
+                />
+              </div>
+            </div>
+          </Col>
+          <Col flex="1">
+            <InputNumber
+              v-model="baseAttr.strokeWidth"
+              @on-change="(value) => changeCommon('strokeWidth', value)"
+              :append="$t('width')"
+              :min="0"
+            ></InputNumber>
+          </Col>
+        </Row>
+      </div>
+    </template>
+
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<style scoped lang="less">
+.color-bar {
+  // width: 30px;
+  height: 30px;
+  cursor: pointer;
+  border: 2px solid #f6f7f9;
+}
+:deep(.ivu-input-number) {
+  display: block;
+  width: 100%;
+}
+
+:deep(.ivu-color-picker) {
+  display: block;
+}
+.ivu-row {
+  margin-bottom: 8px;
+  .ivu-col {
+    position: inherit;
+    &__box {
+      display: flex;
+      align-items: center;
+      background: #f8f8f8;
+      border-radius: 4px;
+      gap: 8px;
+    }
+  }
+
+  .label {
+    padding-left: 8px;
+  }
+  .content {
+    flex: 1;
+    :deep(.--input),
+    :deep(.ivu-select-selection) {
+      background-color: transparent;
+      border: none !important;
+      box-shadow: none !important;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/attributeTextFloat.vue b/src/fabric-editor/components/attributeTextFloat.vue
new file mode 100644
index 0000000..72d7c29
--- /dev/null
+++ b/src/fabric-editor/components/attributeTextFloat.vue
@@ -0,0 +1,172 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-10 17:52:40
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-13 17:08:59
+ * @Description: 灏忔暟鐐逛笅鏍囦笂鏍�
+-->
+
+<template>
+  <div v-if="isOne && isMatchType" class="attr-item-box">
+    <div class="flex-view">
+      <div class="flex-item">
+        <span class="label">{{ $t('textFloat') }}</span>
+        <div class="content">
+          <Select
+            v-model="baseAttr.verticalAlign"
+            @on-change="(value) => changeCommon('verticalAlign', value)"
+          >
+            <Option value="null">鏃�</Option>
+            <Option value="bottom">涓嬫爣</Option>
+            <Option value="top">涓婃爣</Option>
+          </Select>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script name="Price" setup>
+import useSelect from '@/hooks/select';
+
+const baseAttr = reactive({
+  verticalAlign: 'null',
+});
+
+const matchType = ['i-text', 'textbox', 'text'];
+const { isMatchType, canvasEditor, isOne } = useSelect(matchType);
+
+const getObjectAttr = (e) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  // 涓嶆槸褰撳墠obj锛岃烦杩�
+  if (e && e.target && e.target !== activeObject) return;
+  if (activeObject && isMatchType && activeObject?.text?.includes('.')) {
+    baseAttr.verticalAlign = activeObject.get('verticalAlign');
+  }
+};
+
+const changeCommon = (key, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject && activeObject.text.includes('.')) {
+    const [init] = activeObject.text.split('.');
+    const startIndex = init.length + 1;
+    const endIndex = activeObject.text.length;
+    activeObject.styles = [];
+    // 涓婃爣
+    if (value === 'top') {
+      activeObject.setSuperscript(startIndex, endIndex);
+    } else if (value === 'bottom') {
+      // 涓嬫爣
+      activeObject.setSelectionStyles(
+        {
+          fontSize: activeObject.superscript.size * activeObject.fontSize,
+        },
+        startIndex,
+        endIndex
+      );
+    }
+    activeObject.set(key, value);
+    canvasEditor.canvas.renderAll();
+  }
+};
+
+const update = getCurrentInstance();
+const selectCancel = () => {
+  update?.proxy?.$forceUpdate();
+};
+
+onMounted(() => {
+  // 鑾峰彇瀛椾綋鏁版嵁
+  getObjectAttr();
+  canvasEditor.on('selectCancel', selectCancel);
+  canvasEditor.on('selectOne', getObjectAttr);
+  canvasEditor.canvas.on('object:modified', getObjectAttr);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectCancel', selectCancel);
+  canvasEditor.off('selectOne', getObjectAttr);
+  canvasEditor.canvas.off('object:modified', getObjectAttr);
+});
+</script>
+
+<style scoped lang="less">
+.flex-view {
+  width: 100%;
+  margin-bottom: 5px;
+  padding: 5px;
+  display: inline-flex;
+  justify-content: space-between;
+  border-radius: 5px;
+  background: #f6f7f9;
+}
+.flex-item {
+  display: inline-flex;
+  flex: 1;
+  .label {
+    width: 32px;
+    height: 32px;
+    line-height: 32px;
+    display: inline-block;
+    font-size: 14px;
+    // color: #333333;
+  }
+  .content {
+    flex: 1;
+    // width: 60px;
+  }
+  .slider-box {
+    width: calc(100% - 50px);
+    margin-left: 10px;
+  }
+  .left {
+    flex: 1;
+  }
+  .right {
+    flex: 1;
+    margin-left: 10px;
+    :deep(.ivu-input-number) {
+      display: block;
+      width: 100%;
+    }
+  }
+  :deep(.ivu-slider-wrap) {
+    margin: 13px 0;
+  }
+  :deep(.ivu-radio-group-button) {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    & .ivu-radio-wrapper {
+      // width: 48px;
+      flex: 1;
+      line-height: 40px;
+      text-align: center;
+      svg {
+        vertical-align: baseline;
+      }
+    }
+  }
+
+  :deep(.ivu-btn-group) {
+    display: flex;
+    flex: 1;
+    .ivu-btn {
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-btn-group-large) {
+    & > .ivu-btn {
+      font-size: 24px;
+      flex: 1;
+    }
+  }
+
+  :deep(.ivu-radio-group-button) {
+    &.ivu-radio-group-large .ivu-radio-wrapper {
+      font-size: 24px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/bgBar.vue b/src/fabric-editor/components/bgBar.vue
new file mode 100644
index 0000000..15dce3b
--- /dev/null
+++ b/src/fabric-editor/components/bgBar.vue
@@ -0,0 +1,117 @@
+<template>
+  <div v-if="!isSelect">
+    <div class="attr-item-box">
+      <!-- <h3>{{ $t('bgSeting.color') }}</h3> -->
+      <Divider plain orientation="left">
+        <h4>{{ $t('bgSeting.color') }}</h4>
+      </Divider>
+      <Form :label-width="0">
+        <FormItem prop="name">
+          <ColorPicker v-model="color" @on-change="setThisColor" alpha />
+        </FormItem>
+      </Form>
+      <!-- <Divider plain></Divider> -->
+    </div>
+    <div class="attr-item-box">
+      <!-- <h3>{{ $t('bgSeting.colorMacthing') }}</h3> -->
+      <Divider plain orientation="left">
+        <h4>{{ $t('bgSeting.colorMacthing') }}</h4>
+      </Divider>
+      <div class="color-list">
+        <template v-for="(item, i) in colorList" :key="item + i">
+          <span :style="`background:${item}`" @click="setColor(item)"></span>
+        </template>
+      </div>
+    </div>
+
+    <!-- <div>
+      <Divider plain orientation="left">
+        <h4>钂欑増</h4>
+      </Divider>
+
+      <workspaceMask />
+    </div> -->
+  </div>
+</template>
+
+<script setup name="BgBar">
+// import workspaceMask from './workspaceMask.vue';
+import { ref } from 'vue';
+import useSelect from '@/hooks/select';
+const { isSelect, canvasEditor } = useSelect();
+
+const colorList = ref([
+  '#5F2B63',
+  '#B23554',
+  '#F27E56',
+  '#FCE766',
+  '#86DCCD',
+  '#E7FDCB',
+  '#FFDC84',
+  '#F57677',
+  '#5FC2C7',
+  '#98DFE5',
+  '#C2EFF3',
+  '#DDFDFD',
+  '#9EE9D3',
+  '#2FC6C8',
+  '#2D7A9D',
+  '#48466d',
+  '#61c0bf',
+  '#bbded6',
+  '#fae3d9',
+  '#ffb6b9',
+  '#ffaaa5',
+  '#ffd3b6',
+  '#dcedc1',
+  '#a8e6cf',
+]);
+
+const color = ref('rgba(255, 255, 255, 1)');
+// 鑳屾櫙棰滆壊璁剧疆
+const setThisColor = () => {
+  setColor(color.value);
+};
+// 鑳屾櫙棰滆壊璁剧疆
+function setColor(c) {
+  const workspace = canvasEditor.canvas.getObjects().find((item) => item.id === 'workspace');
+  workspace.set('fill', c);
+  canvasEditor.canvas.renderAll();
+  color.value = c;
+}
+
+// 鍔犺浇妯℃澘鏃跺洖鏄鹃鑹插��
+const handleChangeColor = () => {
+  const workspace = canvasEditor.canvas.getObjects().find((item) => item.id === 'workspace');
+  color.value = workspace.fill;
+};
+
+onMounted(() => {
+  canvasEditor.on('loadJson', handleChangeColor);
+});
+
+onUnmounted(() => {
+  canvasEditor.off('loadJson', handleChangeColor);
+});
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-form-item) {
+  margin-bottom: 0;
+  .ivu-color-picker {
+    display: unset;
+  }
+}
+.color-list {
+  display: flex;
+  flex-wrap: wrap;
+  span {
+    height: 30px;
+    width: 30px;
+    border-radius: 15px;
+    border: 3px solid #fff;
+    vertical-align: middle;
+    cursor: pointer;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/centerAlign.vue b/src/fabric-editor/components/centerAlign.vue
new file mode 100644
index 0000000..db7e7fb
--- /dev/null
+++ b/src/fabric-editor/components/centerAlign.vue
@@ -0,0 +1,63 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:39:38
+ * @Description: 澶氬厓绱犳垨鍗曞厓绱犲榻愭柟寮�
+-->
+
+<template>
+  <div v-if="isSelect" class="attr-item-box">
+    <!-- <h3>{{ $t('attrSeting.centerAlign.name') }}</h3> -->
+    <Divider plain orientation="left">
+      <h4>{{ $t('attrSeting.centerAlign.name') }}</h4>
+    </Divider>
+    <div class="bg-item">
+      <!-- 姘村钩闆嗕腑 -->
+      <Tooltip :content="$t('attrSeting.centerAlign.centerX')">
+        <Button long @click="position('centerH')" type="text">
+          <centerX width="14" height="14"></centerX>
+        </Button>
+      </Tooltip>
+      <!-- 姘村钩鍨傜洿灞呬腑 -->
+      <Tooltip :content="$t('attrSeting.centerAlign.center')">
+        <Button long @click="position('center')" type="text">
+          <centerIcon width="14" height="14"></centerIcon>
+        </Button>
+      </Tooltip>
+      <!-- 鍨傜洿灞呬腑 -->
+      <Tooltip :content="$t('attrSeting.centerAlign.centerY')">
+        <Button long @click="position('centerV')" type="text">
+          <centerY width="14" height="14"></centerY>
+        </Button>
+      </Tooltip>
+    </div>
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="CenterAlign">
+import useSelect from '@/fabric-editor/hooks/select';
+import centerIcon from '@/fabric-editor/assets/icon/centerAlign/center.svg?component';
+import centerX from '@/fabric-editor/assets/icon/centerAlign/centerX.svg?component';
+import centerY from '@/fabric-editor/assets/icon/centerAlign/centerY.svg?component';
+
+const { isSelect, canvasEditor } = useSelect();
+
+const position = (name) => {
+  canvasEditor.position(name);
+};
+</script>
+<style scoped lang="scss">
+:deep(.ivu-btn) {
+  &[disabled] {
+    svg {
+      opacity: 0.2;
+    }
+  }
+}
+
+svg {
+  vertical-align: text-bottom;
+}
+</style>
diff --git a/src/fabric-editor/components/clipImage.vue b/src/fabric-editor/components/clipImage.vue
new file mode 100644
index 0000000..c40abee
--- /dev/null
+++ b/src/fabric-editor/components/clipImage.vue
@@ -0,0 +1,90 @@
+<template>
+  <div v-if="isOne && type === 'image'" class="attr-item-box">
+    <div class="bg-item ivu-mb-8">
+      <Dropdown style="width: 270px" @on-click="addClipPath">
+        <Button type="text" long>{{ $t('createClip') }}</Button>
+        <template #list>
+          <DropdownMenu>
+            <DropdownItem v-for="item in options" :key="item.value" :name="item.value">
+              {{ item.label }}
+            </DropdownItem>
+          </DropdownMenu>
+        </template>
+      </Dropdown>
+    </div>
+    <div class="bg-item">
+      <Button @click="removeClip" type="text" long>{{ $t('removeClip') }}</Button>
+    </div>
+  </div>
+</template>
+
+<script setup name="ReplaceImg">
+import useSelect from '@/hooks/select';
+import { useI18n } from 'vue-i18n';
+
+const update = getCurrentInstance();
+// const canvasEditor = inject('canvasEditor');
+const { canvasEditor, isOne } = useSelect();
+const { t } = useI18n();
+const type = ref('');
+const options = [
+  {
+    label: t('polygonClip'),
+    value: 'polygon',
+  },
+  {
+    label: t('rectClip'),
+    value: 'rect',
+  },
+  {
+    label: t('circleClip'),
+    value: 'circle',
+  },
+  {
+    label: t('triangleClip'),
+    value: 'triangle',
+  },
+  {
+    label: t('polygonClipInverted'),
+    value: 'polygon-inverted',
+  },
+  {
+    label: t('rectClipInverted'),
+    value: 'rect-inverted',
+  },
+  {
+    label: t('circleClipInverted'),
+    value: 'circle-inverted',
+  },
+  {
+    label: t('triangleClipInverted'),
+    value: 'triangle-inverted',
+  },
+];
+const addClipPath = async (name) => {
+  canvasEditor.addClipPathToImage(name);
+};
+const removeClip = () => {
+  canvasEditor.removeClip();
+};
+const init = () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    type.value = activeObject.type;
+    update?.proxy?.$forceUpdate();
+  }
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', init);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', init);
+});
+</script>
+<style lang="less" scoped>
+.attr-item-box {
+  margin-top: 8px;
+}
+</style>
diff --git a/src/fabric-editor/components/clone.vue b/src/fabric-editor/components/clone.vue
new file mode 100644
index 0000000..dafb1fc
--- /dev/null
+++ b/src/fabric-editor/components/clone.vue
@@ -0,0 +1,15 @@
+<template>
+  <Tooltip v-if="isOne" :content="$t('quick.copy')">
+    <Button long @click="clone" icon="ios-copy" type="text"></Button>
+  </Tooltip>
+</template>
+
+<script setup name="Clone">
+import useSelect from '@/hooks/select';
+import { debounce } from 'lodash-es';
+
+const { canvasEditor, isOne } = useSelect();
+const clone = debounce(function () {
+  canvasEditor.clone();
+}, 300);
+</script>
diff --git a/src/fabric-editor/components/color-picker/comps/AngleHandle.vue b/src/fabric-editor/components/color-picker/comps/AngleHandle.vue
new file mode 100644
index 0000000..03b3162
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/comps/AngleHandle.vue
@@ -0,0 +1,162 @@
+<!--
+ * @Author: ShawnPhang
+ * @Date: 2023-11-29 10:34:54
+ * @Description: 瑙掑害鎵嬫焺
+ * @LastEditors: ShawnPhang <https://m.palxp.cn>
+ * @LastEditTime: 2023-11-29 19:24:14
+-->
+<template>
+  <div class="angle-input-box">
+    <input
+      ref="numInput"
+      v-model="num"
+      class="angle-input"
+      @focus="visiable = true"
+      @blur="visiable = false"
+      @input="inputChange"
+    />
+    <div
+      v-show="visiable"
+      class="AngleHandle"
+      @mousedown="touch($event, true)"
+      @mouseup="touch($event, false)"
+    >
+      <div class="angle" @mouseup="turn" @mousemove="turn">
+        <div :style="`transform: rotate(${angleInDegrees}deg)`" class="line"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watch, computed } from 'vue';
+
+export default defineComponent({
+  props: ['modelValue'],
+  emits: ['change', 'update:modelValue'],
+  setup(props, { emit }) {
+    const num = ref(90);
+    const numInput = ref(null);
+    const angleInDegrees = computed(() => {
+      return num.value - 90;
+    });
+    let inProcess = false;
+    const visiable = ref(false);
+
+    const inputChange = (e: any) => {
+      emit('change', e);
+    };
+    watch(
+      () => num.value,
+      (v) => {
+        props.modelValue !== num.value && emit('update:modelValue', v);
+        emit('change');
+      }
+    );
+    watch(
+      () => props.modelValue,
+      (v) => {
+        num.value = v;
+      }
+    );
+
+    const turn = (e: any) => {
+      if (!inProcess) {
+        return;
+      }
+      const origin = { x: 27, y: 27 };
+      // 璁$畻鐩稿浜庡師鐐圭殑鍧愭爣宸��
+      const deltaX = e.offsetX - origin.x;
+      const deltaY = e.offsetY - origin.y;
+      // 璁$畻澶硅锛堝姬搴︼級
+      const angleInRadians = Math.atan2(deltaY, deltaX);
+      // 灏嗗姬搴﹁浆鎹负瑙掑害
+      const angleInDegrees = (angleInRadians * 180) / Math.PI;
+      num.value = Math.round(angleInDegrees + 90);
+    };
+
+    const touch = (e: any, isHandle: boolean) => {
+      e.preventDefault();
+      inProcess = isHandle;
+    };
+    return { inputChange, num, turn, touch, angleInDegrees, numInput, visiable };
+  },
+});
+</script>
+
+<style lang="less">
+.angle-input {
+  width: 38px;
+  margin-left: 5px;
+  padding: 0 0 0 4px;
+  border: 1px solid #e8eaec;
+  border-radius: 4px;
+  position: relative;
+}
+.angle-input-box {
+  position: relative;
+}
+.angle-input-box::after {
+  content: '掳';
+  width: 5px;
+  height: 2px;
+  position: absolute;
+  right: 2px;
+  top: 0;
+}
+
+.AngleHandle {
+  position: absolute;
+  z-index: 2;
+  right: 2px;
+  margin-top: 3px;
+  background: #ffffff;
+  width: 60px;
+  height: 60px;
+  border-radius: 7px;
+  box-shadow: 0 0 2px rgb(0 0 0 / 60%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  .angle {
+    width: 54px;
+    height: 54px;
+    position: relative;
+    overflow: hidden;
+    background: #f1f2f4;
+    border-radius: 50%;
+    user-select: none;
+    cursor: pointer;
+  }
+  .line {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 50%;
+    height: 1px;
+    background: #999999;
+    pointer-events: none;
+    transform-origin: left top;
+  }
+  .line::before {
+    position: absolute;
+    content: '';
+    left: -1px;
+    top: -1px;
+    width: 3px;
+    height: 3px;
+    border-radius: 50%;
+    background: #999999;
+  }
+  .line::after {
+    position: absolute;
+    content: '';
+    right: 0;
+    top: -2px;
+    width: 5px;
+    height: 5px;
+    border-radius: 50%;
+    background: #999999;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/color-picker/comps/TabPanel.vue b/src/fabric-editor/components/color-picker/comps/TabPanel.vue
new file mode 100644
index 0000000..51c6f11
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/comps/TabPanel.vue
@@ -0,0 +1,39 @@
+<template>
+  <div class="tab-panel" :style="rootStyle">
+    <slot />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TabPanel',
+};
+</script>
+
+<script setup>
+import { computed, getCurrentInstance, ref } from 'vue';
+
+const vm = getCurrentInstance();
+vm.parent.exposed.tabs.value.push(vm);
+
+defineProps({
+  // Tabs 浼氱敤鍒� label
+  label: {
+    type: String,
+    required: true,
+  },
+});
+
+const active = ref(false);
+const rootStyle = computed(() => ({
+  display: active.value ? 'block' : 'none',
+}));
+
+function changeActive(value) {
+  active.value = value;
+}
+
+defineExpose({
+  changeActive,
+});
+</script>
diff --git a/src/fabric-editor/components/color-picker/comps/Tabs.vue b/src/fabric-editor/components/color-picker/comps/Tabs.vue
new file mode 100644
index 0000000..a3227c5
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/comps/Tabs.vue
@@ -0,0 +1,130 @@
+<template>
+  <div class="my-tabs">
+    <div class="my-tabs__header p-0.5 mb-3 rounded bg-gray-100 cursor-pointers">
+      <div class="my-tabs__header-shell relative flex justify-between">
+        <div
+          v-for="(tab, index) in tabs"
+          :key="tab.props.label"
+          class="my-tab__title relative flex-auto py-1 text-center"
+          :class="{ 'my-active': tab.props.label === value }"
+          @click="onClickTab(tab, index)"
+        >
+          {{ tab.props.label }}
+        </div>
+
+        <div class="my-tab__slider" :style="sliderStyle"></div>
+      </div>
+    </div>
+
+    <div class="my-tabs__content">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Tabs',
+};
+</script>
+
+<script setup>
+import { ref, reactive, onMounted, watch, nextTick } from 'vue';
+
+const props = defineProps({
+  value: {
+    type: String,
+    required: true,
+  },
+});
+const emit = defineEmits(['update:value', 'change']);
+
+watch(
+  () => props.value,
+  () => {
+    changeTab();
+  }
+);
+
+const tabs = ref([]);
+const sliderStyle = reactive({ width: 0, left: 0 });
+let tabWidth = 0;
+onMounted(() => {
+  // 鍒濆鍖栨暟鎹�
+  tabWidth = 100 / tabs.value.length;
+  sliderStyle.width = `${tabWidth}%`;
+
+  changeTab();
+});
+
+let preActiveTabVM = null;
+async function changeTab(index = -1) {
+  if (index < 0) {
+    index = tabs.value.findIndex((vm) => vm.props.label === props.value);
+  }
+  sliderStyle.left = `${tabWidth * index}%`;
+
+  // 鍒囨崲 tab 鍐呭
+  try {
+    await nextTick();
+    preActiveTabVM?.exposed?.changeActive?.(false);
+    preActiveTabVM = tabs.value[index];
+    preActiveTabVM.exposed?.changeActive?.(true);
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+function onClickTab(tab, index) {
+  emit('update:value', tab.props.label);
+  changeTab(index);
+}
+
+defineExpose({ tabs });
+</script>
+
+<style scoped>
+.my-tabs__header {
+  margin-bottom: 0.75rem;
+  border-radius: 0.25rem;
+  --tw-bg-opacity: 1;
+  background-color: rgb(243 244 246 / var(--tw-bg-opacity));
+  padding: 0px;
+  padding: 0.125rem;
+}
+
+.my-tabs__header-shell {
+  justify-content: space-between;
+  position: relative;
+  display: flex;
+}
+
+.my-tab__title {
+  text-align: center;
+  position: relative;
+  flex: 1 1 auto;
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+  z-index: 1;
+}
+.my-tab__title.my-active {
+  font-weight: bold;
+}
+
+.my-tab__slider {
+  position: absolute;
+  bottom: 0px;
+  top: 0px;
+  border-radius: 0.25rem;
+  --tw-bg-opacity: 1;
+  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
+  --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+  --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+  transition-property: all;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+  transition-duration: 150ms;
+}
+</style>
diff --git a/src/fabric-editor/components/color-picker/comps/svg.vue b/src/fabric-editor/components/color-picker/comps/svg.vue
new file mode 100644
index 0000000..67c9464
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/comps/svg.vue
@@ -0,0 +1,28 @@
+<!--
+ * @Author: ShawnPhang
+ * @Date: 2023-05-29 15:41:22
+ * @Description: 鍚哥 SVG
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-21 11:45:15
+-->
+<template>
+  <svg
+    t="1685345224620"
+    class="sd-xggj"
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="8489"
+    width="20"
+    height="20"
+  >
+    <path
+      d="M716.288 140.501333a42.666667 42.666667 0 0 1 60.373333 0l90.496 90.496a42.666667 42.666667 0 0 1 0 60.330667l-120.661333 120.704L595.626667 261.12l120.661333-120.661333zM520.192 185.770667l301.696 301.653333-60.330667 60.373333-301.653333-301.696 60.288-60.330666z"
+      p-id="8490"
+    ></path>
+    <path
+      d="M580.565333 366.762667l-60.373333-60.330667-362.026667 362.026667V853.333333l181.034667-3.84 362.026667-362.026666-60.330667-60.373334-331.861333 331.904-60.373334-60.373333 331.904-331.861333z"
+      p-id="8491"
+    ></path>
+  </svg>
+</template>
diff --git a/src/fabric-editor/components/color-picker/index.css b/src/fabric-editor/components/color-picker/index.css
new file mode 100644
index 0000000..cd08509
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/index.css
@@ -0,0 +1,107 @@
+/* ! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com */
+
+/*
+Some configurations are extracted, and tailwindcss are not used in this project
+*/
+
+*,
+::before,
+::after {
+  --tw-border-spacing-x: 0;
+  --tw-border-spacing-y: 0;
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  --tw-pan-x: ;
+  --tw-pan-y: ;
+  --tw-pinch-zoom: ;
+  --tw-scroll-snap-strictness: proximity;
+  --tw-gradient-from-position: ;
+  --tw-gradient-via-position: ;
+  --tw-gradient-to-position: ;
+  --tw-ordinal: ;
+  --tw-slashed-zero: ;
+  --tw-numeric-figure: ;
+  --tw-numeric-spacing: ;
+  --tw-numeric-fraction: ;
+  --tw-ring-inset: ;
+  --tw-ring-offset-width: 0px;
+  --tw-ring-offset-color: #fff;
+  --tw-ring-color: rgb(59 130 246 / 0.5);
+  --tw-ring-offset-shadow: 0 0 #0000;
+  --tw-ring-shadow: 0 0 #0000;
+  --tw-shadow: 0 0 #0000;
+  --tw-shadow-colored: 0 0 #0000;
+  --tw-blur: ;
+  --tw-brightness: ;
+  --tw-contrast: ;
+  --tw-grayscale: ;
+  --tw-hue-rotate: ;
+  --tw-invert: ;
+  --tw-saturate: ;
+  --tw-sepia: ;
+  --tw-drop-shadow: ;
+  --tw-backdrop-blur: ;
+  --tw-backdrop-brightness: ;
+  --tw-backdrop-contrast: ;
+  --tw-backdrop-grayscale: ;
+  --tw-backdrop-hue-rotate: ;
+  --tw-backdrop-invert: ;
+  --tw-backdrop-opacity: ;
+  --tw-backdrop-saturate: ;
+  --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+  --tw-border-spacing-x: 0;
+  --tw-border-spacing-y: 0;
+  --tw-translate-x: 0;
+  --tw-translate-y: 0;
+  --tw-rotate: 0;
+  --tw-skew-x: 0;
+  --tw-skew-y: 0;
+  --tw-scale-x: 1;
+  --tw-scale-y: 1;
+  --tw-pan-x: ;
+  --tw-pan-y: ;
+  --tw-pinch-zoom: ;
+  --tw-scroll-snap-strictness: proximity;
+  --tw-gradient-from-position: ;
+  --tw-gradient-via-position: ;
+  --tw-gradient-to-position: ;
+  --tw-ordinal: ;
+  --tw-slashed-zero: ;
+  --tw-numeric-figure: ;
+  --tw-numeric-spacing: ;
+  --tw-numeric-fraction: ;
+  --tw-ring-inset: ;
+  --tw-ring-offset-width: 0px;
+  --tw-ring-offset-color: #fff;
+  --tw-ring-color: rgb(59 130 246 / 0.5);
+  --tw-ring-offset-shadow: 0 0 #0000;
+  --tw-ring-shadow: 0 0 #0000;
+  --tw-shadow: 0 0 #0000;
+  --tw-shadow-colored: 0 0 #0000;
+  --tw-blur: ;
+  --tw-brightness: ;
+  --tw-contrast: ;
+  --tw-grayscale: ;
+  --tw-hue-rotate: ;
+  --tw-invert: ;
+  --tw-saturate: ;
+  --tw-sepia: ;
+  --tw-drop-shadow: ;
+  --tw-backdrop-blur: ;
+  --tw-backdrop-brightness: ;
+  --tw-backdrop-contrast: ;
+  --tw-backdrop-grayscale: ;
+  --tw-backdrop-hue-rotate: ;
+  --tw-backdrop-invert: ;
+  --tw-backdrop-opacity: ;
+  --tw-backdrop-saturate: ;
+  --tw-backdrop-sepia: ;
+}
diff --git a/src/fabric-editor/components/color-picker/index.vue b/src/fabric-editor/components/color-picker/index.vue
new file mode 100644
index 0000000..c70c505
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/index.vue
@@ -0,0 +1,791 @@
+<!--
+ * @Author: ShawnPhang
+ * @Date: 2023-05-26 17:42:26
+ * @Description: 璋冭壊鏉�
+ * @LastEditors: June 1601745371@qq.com
+ * @LastEditTime: 2024-06-20 17:46:09
+-->
+<template>
+  <div class="color-picker">
+    <Tabs v-if="modes.length > 1" :value="mode" @update:value="onChangeMode">
+      <TabPanel v-for="label in modes" :key="label" :label="label"></TabPanel>
+    </Tabs>
+    <div v-else class="title">{{ mode }}</div>
+
+    <template v-if="showGradient">
+      <div v-show="mode === '娓愬彉'" class="cp__gradient flex-center">
+        <div class="cp__gradient-bar">
+          <div
+            ref="elGradientTrack"
+            class="cpgb__track"
+            style="width: 100%"
+            :style="{ background: value }"
+          >
+            <!-- tabindex="-1" 鏄厓绱犲彲浠ヨЕ鍙� keydown 浜嬩欢 -->
+            <div
+              v-for="(gradient, index) in gradients"
+              :key="index"
+              :class="[
+                'cpgb__pointer',
+                {
+                  'cpgb__pointer--active': gradient === activeGradient,
+                },
+              ]"
+              :data-sort="index"
+              :style="{
+                left: `${gradient.offset * 100}%`,
+                background: gradient.color,
+              }"
+              tabindex="-1"
+              @mousedown="onMousedownGradientPointer(gradient)"
+              @keydown.stop="onKeyupGradientPointer"
+            ></div>
+          </div>
+        </div>
+        <AngleHandleVue v-model="angle" @change="angleChange" />
+      </div>
+    </template>
+
+    <div ref="elPalette" class="cp__palette" :style="{ background: paletteBackground }">
+      <div class="cpp__color-saturation"></div>
+      <div class="cpp__color-value"></div>
+      <div ref="elPalettePointer" class="cpp__pointer"></div>
+    </div>
+
+    <div ref="elSliderHux" class="cp__slider cp__slider-hux">
+      <div class="cps__track">
+        <div ref="elSliderHuxPointer" class="cpst__pointer"></div>
+      </div>
+    </div>
+
+    <div ref="elSliderAlpha" class="cp__slider cp__slider-alpha">
+      <div class="cpsa__background" :style="sliderAlphaBackgroundStyle"></div>
+      <div class="cps__track">
+        <div ref="elSliderAlphaPointer" class="cpst__pointer"></div>
+      </div>
+    </div>
+
+    <div class="cp__box">
+      <div class="item" @click="onClickStraw">
+        <xiguan v-if="hasEyeDrop" />
+        <input v-else class="native" type="color" @input="onClickStraw" />
+      </div>
+      <!-- <input :value="value" @input="$emit('update:value', $event.target.value)" class="input" /> -->
+      <input v-if="mode === '娓愬彉'" class="input" :value="activeGradient.color" />
+      <input v-else :value="value" class="input" @blur="onInputBlur" />
+      <template v-if="mode === '绾壊'">
+        <div
+          v-for="pc in predefine"
+          :key="pc"
+          class="item item-color"
+          :style="{ background: pc }"
+          @click="onClickStraw({ target: { value: pc } })"
+        ></div>
+      </template>
+      <!-- <input :value="alpha" class="w-12" size="small" :min="0" :max="100" @input="onChangeAlpha" @change="onChangeAlpha" /> -->
+    </div>
+  </div>
+</template>
+
+<script>
+import './index.css';
+export default {
+  name: 'ColorPicker',
+  inheritAttrs: false,
+};
+</script>
+
+<script setup>
+// import { ref, reactive, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
+import { registerMoveableElement } from './utils/moveable.ts';
+import { hexA2HSLA, HSLA2HexA, hex2RGB, RGB2HSL, hexA2RGBA, RGBA2HexA } from './utils/color.ts';
+import { toGradientString, parseBackgroundValue, toolTip } from './utils/helper.ts';
+import Tabs from './comps/Tabs.vue';
+import xiguan from './comps/svg.vue';
+import TabPanel from './comps/TabPanel.vue';
+import { debounce } from 'lodash-es';
+import AngleHandleVue from './comps/AngleHandle.vue';
+
+const props = defineProps({
+  value: {
+    type: String,
+    default: '#ffffffff',
+  },
+
+  modes: {
+    type: Array,
+    default: () => ['绾壊', '娓愬彉'], // 鍥炬
+  },
+
+  defaultColor: {
+    type: String,
+    default: '#ffffffff',
+  },
+
+  defaultGradient: {
+    type: String,
+    default: 'linear-gradient(90deg, #fffae0ff 0%, #ffd1f1ff 100%)',
+  },
+
+  defaultImage: {
+    type: String,
+    default: 'https://st0.dancf.com/csc/157/material-2d-textures/0/20190714-174653-ed3c.jpg',
+  },
+});
+
+const emit = defineEmits(['update:value', 'change', 'native-pick', 'blur']);
+
+const mode = ref(parseBackgroundValue(props.value)); // 棰滆壊銆佹笎鍙樸�佸浘鐗�
+const angle = ref(90);
+const gradients = ref([]);
+const hsla = reactive({ h: 0, s: 0, l: 0, a: 0 });
+const paletteBackground = ref('#f00');
+const hex = ref('#000');
+const alpha = ref(0);
+let activeGradient = ref({});
+const hasEyeDrop = 'EyeDropper' in window;
+
+const elGradientTrack = ref();
+const elPalette = ref();
+const elPalettePointer = ref();
+const elSliderHuxPointer = ref();
+const elSliderHux = ref();
+const elSliderAlphaPointer = ref();
+const elSliderAlpha = ref();
+// const elStrawCanvas = ref();
+
+let gradientMoveable = null;
+let paletteMoveable = null;
+let sliderHuxMoveable = null;
+let sliderAlphaMoveable = null;
+let mousedownGradientPointer = null;
+let backendHex = null;
+// 鏄惁鍙互鏀瑰彉 palette sliderHux sliderAlpha 鐨� pointer 浣嶇疆
+let canChangeHSLAPointerPos = true;
+let canChangeHSLAPointerPosTimer = null;
+
+const predefine = ref([]); // 鍘嗗彶璁板綍
+
+const record = {
+  color: props.defaultColor,
+  gradient: props.defaultGradient,
+  image: props.defaultImage,
+};
+
+const showGradient = computed(() => {
+  return props.modes.includes('娓愬彉');
+});
+
+const sliderAlphaBackgroundStyle = computed(() => {
+  const rgb = hex2RGB(hex.value).join(',');
+  return {
+    background: `linear-gradient(to right, rgba(${rgb}, 0) 0%, rgb(${rgb}) 100%)`,
+  };
+});
+
+watch(activeGradient, (value) => {
+  setColor(value.color);
+});
+
+watch(hex, (value) => {
+  onChangeHex(value);
+});
+
+watch(
+  () => props.value,
+  (value) => {
+    const _mode = parseBackgroundValue(value);
+    if (_mode !== mode.value) {
+      mode.value = _mode;
+    }
+    changeMode(_mode);
+    recordValue(value);
+    addHistory(value);
+  }
+);
+
+// TODO: 娣诲姞閫夋嫨鍘嗗彶璁板綍
+const addHistory = debounce(async (value) => {
+  const history = predefine.value;
+  // 濡傛灉宸茬粡瀛樺湪灏辨彁鍒板墠闈㈡潵锛岄伩鍏嶉噸澶�
+  const index = history.indexOf(value);
+  if (index !== -1) {
+    predefine.value.splice(index, 1);
+  }
+  if (history.length >= 4) {
+    predefine.value.splice(history.length - 1, 1);
+  }
+  // 鎶婃渶鏂扮殑棰滆壊鏀惧湪澶撮儴
+  const head = [value];
+  predefine.value = head.concat(history);
+}, 300);
+
+const unwatchHSLA = watch(hsla, onChangeHSLA, { deep: true });
+function onChangeHSLA(newHsla) {
+  const hexA = HSLA2HexA(...Object.values(newHsla));
+
+  let value;
+  if (mode.value === '绾壊') {
+    value = hexA;
+  } else if (mode.value === '娓愬彉') {
+    activeGradient.value.color = hexA;
+    value = toGradientString(angle.value, gradients.value);
+  }
+  updateColorData(hexA);
+  updateValue(value);
+}
+
+onMounted(onMountedCallback);
+async function onMountedCallback() {
+  elPalettePointer.value.style.left = `${hsla.s}%`;
+  elPalettePointer.value.style.top = `${100 - hsla.l}%`;
+  elSliderHuxPointer.value.style.left = `${(hsla.h / 360) * 100}%`;
+  elSliderAlphaPointer.value.style.left = `${hsla.a * 100}%`;
+
+  if (showGradient.value) {
+    gradientMoveable = registerMoveableElement(elGradientTrack.value, {
+      onmousedown: onMousedownGradient,
+      onmousemove: onMousemoveGradient,
+      onmouseup: onMouseupGradient,
+    });
+  }
+
+  function onMousedownGradient(position) {
+    if (mousedownGradientPointer) {
+      return;
+    }
+
+    const index = gradients.value.findIndex((stop) => stop.offset >= position.x);
+    const start = gradients.value[index - 1];
+    const startRGBA = hexA2RGBA(start.color);
+    const end = gradients.value[index];
+    const endRGBA = hexA2RGBA(end.color);
+
+    const rgb = [];
+    for (let i = 0; i < 3; i += 1) {
+      rgb.push(startRGBA[i] + (endRGBA[i] - startRGBA[i]) * position.x);
+    }
+
+    const a = end.offset - position.x - (position.x - start.offset) > 0 ? startRGBA[3] : endRGBA[3];
+
+    const color = RGBA2HexA(...rgb, a);
+    activeGradient.value = {
+      color,
+      offset: position.x,
+    };
+
+    gradients.value.splice(index, 0, activeGradient.value);
+  }
+
+  function onMousemoveGradient(position) {
+    if (!mousedownGradientPointer) return;
+
+    activeGradient.value.offset = position.x;
+    gradients.value.sort((a, b) => a.offset - b.offset);
+
+    const value = toGradientString(angle.value, gradients.value);
+    updateValue(value);
+  }
+
+  function onMouseupGradient() {
+    mousedownGradientPointer = false;
+  }
+
+  paletteMoveable = registerMoveableElement(elPalette.value, {
+    onmousemove: onChangeSL,
+    onmouseup: onChangeSL,
+  });
+
+  function onChangeSL(position) {
+    disableChangeHSLA();
+
+    try {
+      const x = position.x * 100;
+      const y = position.y * 100;
+
+      hsla.s = Math.round(x);
+      hsla.l = Math.round(100 - y);
+
+      elPalettePointer.value.style.left = `${x}%`;
+      elPalettePointer.value.style.top = `${y}%`;
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
+  sliderHuxMoveable = registerMoveableElement(elSliderHux.value, {
+    onmousemove: onChangeHux,
+    onmouseup: onChangeHux,
+  });
+
+  function onChangeHux(position) {
+    disableChangeHSLA();
+
+    hsla.h = position.x * 360;
+    elSliderHuxPointer.value.style.left = `${position.x * 100}%`;
+  }
+
+  sliderAlphaMoveable = registerMoveableElement(elSliderAlpha.value, {
+    onmousemove: onChangeAlpha,
+    onmouseup: onChangeAlpha,
+  });
+
+  function onChangeAlpha(position) {
+    disableChangeHSLA();
+
+    hsla.a = position.x;
+    elSliderAlphaPointer.value.style.left = `${position.x * 100}%`;
+  }
+
+  changeMode(mode.value);
+  recordValue(props.value);
+}
+
+onBeforeUnmount(() => {
+  paletteMoveable?.destroy();
+  sliderHuxMoveable?.destroy();
+  sliderAlphaMoveable?.destroy();
+  unwatchHSLA();
+
+  if (gradientMoveable) {
+    gradientMoveable.destroy();
+  }
+});
+
+function recordValue(value) {
+  if (mode.value === '绾壊') {
+    record.color = value;
+  } else if (mode.value === '娓愬彉') {
+    record.gradient = value;
+  } else if (mode.value === '鍥炬') {
+    record.image = value;
+  }
+}
+
+function updateValue(value) {
+  // 绾壊鏃秜alue鍜宲rops.value 涓�鏍峰鑷翠笉鏇存柊
+  // if (value === props.value) return;
+  recordValue(value);
+  emit('update:value', value);
+
+  emit('change', {
+    mode: mode.value,
+    color: value,
+    angle: Number(angle.value),
+    stops: gradients.value,
+  });
+}
+
+async function onChangeMode(value) {
+  if (value === mode.value) return;
+  mode.value = value;
+
+  let color;
+  if (value === '绾壊') {
+    color = record.color;
+  } else if (value === '娓愬彉') {
+    color = record.gradient;
+  } else if (value === '鍥炬') {
+    color = record.image;
+  }
+  updateValue(color);
+}
+
+function changeMode(mode) {
+  if (mode === '绾壊') {
+    setColor(props.value);
+  } else if (mode === '娓愬彉') {
+    if (gradients.value.length === 0) {
+      props.value.match(/[^,]+/g).forEach((item, index) => {
+        if (index === 0) {
+          angle.value = Number(item.match(/\d+/)[0]);
+          return;
+        }
+
+        let [color, offset] = item.trim().split(' ');
+        if (!color.startsWith('#')) color = RGBA2HexA(color);
+
+        offset = offset.match(/\d+/)[0] / 100;
+        gradients.value.push({ color, offset });
+        activeGradient.value = gradients.value[0];
+      });
+    } else {
+      setColor(activeGradient.value.color);
+    }
+  }
+
+  // TODO: 鍥炬
+}
+
+function updateColorData(hexA) {
+  paletteBackground.value = `hsl(${hsla.h}, 100%, 50%)`;
+  hex.value = hexA.slice(0, 7);
+  backendHex = hex.value;
+  alpha.value = Math.round((hsla.a ?? 1) * 100);
+}
+
+function setColor(color) {
+  // 閫氳繃 palette sliderHux sliderAlpha 浜や簰鏀瑰彉 pointer 浣嶇疆
+  // 宸茬粡鏀瑰彉 hsla 鐨勫�煎苟瑙﹀彂 update:value
+  // watch props.value 鍐嶈皟鐢ㄥ綋鍓嶆柟娉曟椂鏃犻渶鍐嶆洿鏂� hsla
+  if (canChangeHSLAPointerPos) {
+    const _hsla = hexA2HSLA(color);
+    hsla.h = _hsla[0];
+    hsla.s = _hsla[1];
+    hsla.l = _hsla[2];
+    hsla.a = _hsla[3];
+
+    updateColorData(color);
+    try {
+      let x = hsla.s;
+      const y = Math.round(100 - hsla.l);
+      elPalettePointer.value.style.left = `${x}%`;
+      elPalettePointer.value.style.top = `${y}%`;
+
+      x = hsla.h / 360;
+      elSliderHuxPointer.value.style.left = `${x * 100}%`;
+
+      elSliderAlphaPointer.value.style.left = `${hsla.a * 100}%`;
+    } catch (error) {
+      console.log(error);
+    }
+  }
+}
+
+function onMousedownGradientPointer(stop) {
+  mousedownGradientPointer = true;
+  activeGradient.value = stop;
+}
+
+function onKeyupGradientPointer(event) {
+  event.stopPropagation();
+  event.preventDefault();
+  if (!['Backspace', 'Delete'].includes(event.key)) return;
+  if (gradients.value.length === 2) return;
+
+  const index = gradients.value.indexOf(activeGradient.value);
+  gradients.value.splice(index, 1);
+  activeGradient.value = gradients.value[0];
+}
+
+function onChangeHex(value) {
+  if (/^#(?:[0-9a-f]{3}){1,2}$/i.test(value)) {
+    const rgb = hex2RGB(value);
+    const [h, s, l] = RGB2HSL(...rgb);
+    hsla.h = h;
+    hsla.s = s;
+    hsla.l = l;
+
+    try {
+      elPalettePointer.value.style.left = `${hsla.s}%`;
+      elPalettePointer.value.style.top = `${100 - hsla.l}%`;
+      elSliderHuxPointer.value.style.left = `${(hsla.h / 360) * 100}%`;
+
+      hex.value = value;
+    } catch (error) {
+      console.log(error);
+    }
+  } else {
+    // hex.value = backendHex
+  }
+}
+
+// function onChangeAlpha(value) {
+// hsla.a = value / 100;
+//  elSliderAlphaPointer.value.style.left = `${value}%`;
+// }
+
+function disableChangeHSLA() {
+  canChangeHSLAPointerPos = false;
+
+  if (canChangeHSLAPointerPosTimer) clearTimeout(canChangeHSLAPointerPosTimer);
+  canChangeHSLAPointerPosTimer = setTimeout(() => {
+    canChangeHSLAPointerPos = true;
+  }, 16);
+}
+
+async function onClickStraw(val) {
+  let result = '';
+  if (val && val.target.value) {
+    const color = val.target.value;
+    result = color + (color.length === 7 ? 'ff' : '');
+  } else {
+    const eyeDropper = new window.EyeDropper(); // 鍒濆鍖栦竴涓狤yeDropper瀵硅薄
+    toolTip('鎸塃sc鍙��鍑�');
+    try {
+      const drop = await eyeDropper.open(); // 寮�濮嬫嬀鍙栭鑹�
+      const colorHexValue = drop.sRGBHex;
+      result = colorHexValue + 'ff';
+    } catch (e) {
+      console.log('鐢ㄦ埛鍙栨秷浜嗗彇鑹�');
+    }
+  }
+  if (mode.value === '娓愬彉') {
+    activeGradient.value.color = result;
+    activeGradient.value = { ...activeGradient.value };
+  } else {
+    emit('update:value', result);
+  }
+  emit('native-pick', result);
+}
+
+const onInputBlur = (e) => {
+  const fixColor = patchHexColor(e.target.value);
+  emit('blur', fixColor);
+  emit('update:value', fixColor);
+};
+
+function patchHexColor(str) {
+  let hex = str.replace(/\s/g, ''); // 绉婚櫎绌烘牸
+  if (!str.startsWith('#')) {
+    hex = '#' + hex;
+  }
+  if (hex.length < 9) {
+    hex = hex.padEnd(9, 'f');
+  }
+  return hex;
+}
+
+function angleChange() {
+  updateValue(toGradientString(angle.value, gradients.value));
+}
+
+defineExpose({
+  updateValue,
+});
+</script>
+
+<style lang="less" scoped>
+*,
+::before,
+::after {
+  box-sizing: border-box;
+  border-width: 0;
+  border-style: solid;
+  border-color: #e5e7eb;
+}
+.flex-center {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.title {
+  margin-bottom: 0.75rem;
+  font-size: 15px;
+  font-weight: 600;
+}
+.color-picker {
+  -webkit-user-select: none;
+  user-select: none;
+  min-width: 220px;
+}
+
+.cp__gradient {
+  &-bar {
+    display: flex;
+    justify-content: center;
+    height: 16px;
+    width: 100%;
+    padding: 0 8px;
+  }
+}
+
+.cpgb__track {
+  position: relative;
+  cursor: pointer;
+}
+
+.cpgb__pointer {
+  cursor: grab;
+  position: absolute;
+  top: -0px;
+  top: -0.125rem;
+  height: 1.25rem;
+  --tw-translate-x: -50%;
+  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
+    skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+    scaleY(var(--tw-scale-y));
+  border-width: 2px;
+  border-style: solid;
+  --tw-border-opacity: 1;
+  border-color: rgb(255 255 255 / var(--tw-border-opacity));
+  --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+  --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+  outline: 2px solid transparent;
+  outline-offset: 2px;
+  width: 18px;
+
+  &--active {
+    z-index: 1;
+    border-radius: 3px;
+    box-shadow: 0 0 4px 0 rgb(0 0 0 / 20%), 0 0 0 1.2px #2254f4;
+  }
+}
+
+.cp__palette {
+  height: 140px;
+  position: relative;
+  margin-top: 0.75rem;
+  margin-top: 0.875rem;
+  cursor: pointer;
+  overflow: hidden;
+  border-radius: 0.25rem;
+  .cpp__color-saturation,
+  .cpp__color-value {
+    position: absolute;
+    bottom: 0px;
+    right: 0px;
+    top: 0px;
+    width: 100%;
+    height: 100%;
+  }
+
+  .cpp__color-saturation {
+    background-image: linear-gradient(to right, var(--tw-gradient-stops));
+    --tw-gradient-from: #fff var(--tw-gradient-from-position);
+    --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
+    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+  }
+
+  .cpp__color-value {
+    background-image: linear-gradient(to top, var(--tw-gradient-stops));
+    --tw-gradient-from: #000 var(--tw-gradient-from-position);
+    --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);
+    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+  }
+
+  .cpp__pointer {
+    position: absolute;
+    height: 0.75rem;
+    width: 0.75rem;
+    --tw-translate-x: -0.25rem;
+    transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
+      skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+      scaleY(var(--tw-scale-y));
+    --tw-translate-x: -0.375rem;
+    transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
+      skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+      scaleY(var(--tw-scale-y));
+    --tw-translate-y: -0.25rem;
+    transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
+      skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+      scaleY(var(--tw-scale-y));
+    border-radius: 9999px;
+    border-width: 2px;
+    --tw-border-opacity: 1;
+    border-color: rgb(255 255 255 / var(--tw-border-opacity));
+    --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+    --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
+    box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+      var(--tw-shadow);
+  }
+}
+
+.cp__slider {
+  position: relative;
+  margin-top: 0.75rem;
+  margin-top: 0.875rem;
+  height: 0.5rem;
+  border-radius: 0.25rem;
+  &-hux {
+    background: linear-gradient(
+      90deg,
+      red 0,
+      #ff0 17%,
+      #0f0 33%,
+      #0ff 50%,
+      #00f 67%,
+      #f0f 83%,
+      red
+    );
+  }
+
+  &-alpha {
+    background: linear-gradient(
+        to top right,
+        hsla(0, 0%, 80%, 0.4) 25%,
+        transparent 0,
+        transparent 75%,
+        hsla(0, 0%, 80%, 0.4) 0,
+        hsla(0, 0%, 80%, 0.4)
+      ),
+      linear-gradient(
+        to top right,
+        hsla(0, 0%, 80%, 0.4) 25%,
+        transparent 0,
+        transparent 75%,
+        hsla(0, 0%, 80%, 0.4) 0,
+        hsla(0, 0%, 80%, 0.4)
+      );
+    background-size: 6px 6px;
+    background-position: 0 0, 3px 3px;
+  }
+
+  .cpsa__background {
+    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
+    height: 100%;
+    border-radius: 0.25rem;
+  }
+}
+.cp__box {
+  margin-top: 0.75rem;
+  margin-top: 0.875rem;
+  display: flex;
+  .item {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    margin-left: 6px;
+    width: 24px;
+    height: 24px;
+    box-sizing: border-box;
+    border-radius: 4px;
+  }
+  .item-color {
+    box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
+  }
+  .item:first-of-type {
+    margin: 0;
+  }
+  .item:hover {
+    transform: scale(1.08);
+  }
+  .input {
+    width: 4.7rem;
+    margin-left: 2px;
+  }
+  .native {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.cps__track {
+  position: absolute;
+  left: 0.25rem;
+  right: 0.25rem;
+  top: 0px;
+}
+
+.cpst__pointer {
+  cursor: pointer;
+  box-shadow: 0 0 2px rgb(0 0 0 / 60%);
+  position: absolute;
+  top: 0px;
+  box-sizing: content-box;
+  height: 0.5rem;
+  width: 0.5rem;
+  --tw-translate-x: -0.5rem;
+  --tw-translate-y: -0.25rem;
+  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
+    skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
+    scaleY(var(--tw-scale-y));
+  border-radius: 9999px;
+  border-width: 4px;
+  --tw-border-opacity: 1;
+  border-color: rgb(255 255 255 / var(--tw-border-opacity));
+}
+</style>
diff --git a/src/fabric-editor/components/color-picker/utils/color.ts b/src/fabric-editor/components/color-picker/utils/color.ts
new file mode 100644
index 0000000..a875700
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/utils/color.ts
@@ -0,0 +1,130 @@
+export const RGB2Hex = (r: number, g: number, b: number) => {
+  let _r = Math.round(r).toString(16);
+  let _g = Math.round(g).toString(16);
+  let _b = Math.round(b).toString(16);
+
+  if (_r.length === 1) _r = '0' + _r;
+  if (_g.length === 1) _g = '0' + _g;
+  if (_b.length === 1) _b = '0' + _b;
+
+  return '#' + _r + _g + _b;
+};
+
+export const RGBA2HexA = (r: number, g: number, b: number, a = 1) => {
+  const hex = RGB2Hex(r, g, b);
+
+  let _a = Math.round((a as number) * 255).toString(16);
+  if (_a.length === 1) _a = '0' + _a;
+
+  return hex + _a;
+};
+
+export const RGB2HSL = (r: number, g: number, b: number) => {
+  r /= 255;
+  g /= 255;
+  b /= 255;
+
+  const minVal = Math.min(r, g, b);
+  const maxVal = Math.max(r, g, b);
+  const delta = maxVal - minVal;
+
+  let h = 0;
+  let s = 0;
+  const l = maxVal;
+  if (delta === 0) {
+    h = s = 0;
+  } else {
+    s = delta / maxVal;
+    const dr = ((maxVal - r) / 6 + delta / 2) / delta;
+    const dg = ((maxVal - g) / 6 + delta / 2) / delta;
+    const db = ((maxVal - b) / 6 + delta / 2) / delta;
+
+    if (r === maxVal) {
+      h = db - dg;
+    } else if (g === maxVal) {
+      h = 1 / 3 + dr - db;
+    } else if (b === maxVal) {
+      h = 2 / 3 + dg - dr;
+    }
+
+    if (h < 0) {
+      h += 1;
+    } else if (h > 1) {
+      h -= 1;
+    }
+  }
+
+  return [h * 360, s * 100, l * 100];
+};
+
+export const RGBA2HSLA = (r: number, g: number, b: number, a = 1) => [...RGB2HSL(r, g, b), a];
+
+export function HSL2RGB(h: number, s: number, l: number) {
+  h = (h / 360) * 6;
+  s /= 100;
+  l /= 100;
+
+  const i = Math.floor(h);
+
+  const f = h - i;
+  const p = l * (1 - s);
+  const q = l * (1 - f * s);
+  const t = l * (1 - (1 - f) * s);
+
+  const mod = i % 6;
+  const r = [l, q, p, p, t, l][mod];
+  const g = [t, l, l, q, p, p][mod];
+  const b = [p, p, t, l, l, q][mod];
+
+  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+export const HSLA2RGBA = (h: number, s: number, l: number, a = 1) => [...HSL2RGB(h, s, l), a];
+
+export const HSL2Hex = (h: number, s: number, l: number) => {
+  const [r, g, b] = HSL2RGB(h, s, l);
+  return RGB2Hex(r, g, b);
+};
+
+export const HSLA2HexA = (h: number, s: number, l: number, a = 1) => {
+  const hex = HSL2Hex(h, s, l);
+  return `${hex}${a === 0 ? '00' : Math.round(a * 255).toString(16)}`;
+};
+
+export const hex2RGB = (hex: string) => {
+  hex = hex.slice(0, 7);
+
+  let r = 0;
+  let g = 0;
+  let b = 0;
+
+  if (hex.length == 4) {
+    // 3 digits
+    r = Number('0x' + hex[1] + hex[1]);
+    g = Number('0x' + hex[2] + hex[2]);
+    b = Number('0x' + hex[3] + hex[3]);
+  } else if (hex.length == 7) {
+    // 6 digits
+    r = Number('0x' + hex[1] + hex[2]);
+    g = Number('0x' + hex[3] + hex[4]);
+    b = Number('0x' + hex[5] + hex[6]);
+  }
+
+  return [r, g, b];
+};
+
+export const hexA2RGBA = (hexA: string) => {
+  const rgb = hex2RGB(hexA);
+  const a = Number('0x' + hexA[7] + hexA[8]);
+  return [...rgb, Number((a / 255).toFixed(2))];
+};
+
+export const hex2HSL = (hex: string) => {
+  const [r, g, b] = hex2RGB(hex);
+  return RGB2HSL(r, g, b);
+};
+
+export const hexA2HSLA = (hexA: string) => {
+  const [r, g, b, a] = hexA2RGBA(hexA);
+  return RGBA2HSLA(r, g, b, a);
+};
diff --git a/src/fabric-editor/components/color-picker/utils/helper.ts b/src/fabric-editor/components/color-picker/utils/helper.ts
new file mode 100644
index 0000000..4cd2ff3
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/utils/helper.ts
@@ -0,0 +1,58 @@
+/*
+ * @Author: ShawnPhang
+ * @Date: 2023-04-26 11:30:10
+ * @Description:
+ * @LastEditors: ShawnPhang <https://m.palxp.cn>
+ * @LastEditTime: 2023-11-28 11:03:14
+ */
+export const parseBackgroundValue = (value: string): string => {
+  if (value.startsWith('#')) return '绾壊';
+  if (value.startsWith('linear-gradient')) return '娓愬彉';
+  return '鍥炬';
+};
+
+interface Stop {
+  color: string;
+  offset: number;
+}
+
+export const toGradientString = (angle: number, stops: Stop[]) => {
+  const s: string[] = [];
+  stops.forEach((stop) => {
+    s.push(`${stop.color} ${stop.offset * 100}%`);
+  });
+  return `linear-gradient(${angle}deg, ${s.join(',')})`;
+};
+
+/**
+ * 鏄剧ず鍏ㄥ眬鎻愮ず
+ * @param content
+ * @param tooltipVisible
+ * @returns
+ */
+export function toolTip(content: string) {
+  const tooltip = drawTooltip(content);
+  document.body.appendChild(tooltip);
+  setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000);
+}
+
+function drawTooltip(content: string, tooltipVisible = true) {
+  const tooltip: any = document.createElement('div');
+  tooltip.id = 'color-pipette-tooltip-container';
+  tooltip.innerHTML = content;
+  tooltip.style = `
+    position: fixed;
+    left: 50%;
+    top: 9%;
+    z-index: 10002;
+    display: ${tooltipVisible ? 'flex' : 'none'};
+    align-items: center;
+    background-color: rgba(0,0,0,0.4);
+    padding: 6px 12px;
+    border-radius: 4px;
+    color: #fff;
+    font-size: 18px;
+    pointer-events: none;
+  `;
+  return tooltip;
+}
diff --git a/src/fabric-editor/components/color-picker/utils/moveable.ts b/src/fabric-editor/components/color-picker/utils/moveable.ts
new file mode 100644
index 0000000..c176b11
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/utils/moveable.ts
@@ -0,0 +1,73 @@
+import { toNumber } from './tool';
+
+interface Position {
+  x: number;
+  y: number;
+}
+
+interface RegisterMoveablePanelOptions {
+  wrapEl?: HTMLElement;
+  onmousedown?(position: Position, event: MouseEvent): void;
+  onmousemove?(position: Position, event: MouseEvent): void;
+  onmouseup?(position: Position, event: MouseEvent): void;
+}
+
+export const registerMoveableElement = (
+  el: HTMLElement,
+  { onmousedown, onmousemove, onmouseup }: RegisterMoveablePanelOptions = {}
+) => {
+  let elRect = el.getBoundingClientRect();
+  const position = { x: 0, y: 0 };
+
+  const update = (event: MouseEvent) => {
+    let dx = event.pageX - elRect.x;
+    let dy = event.pageY - elRect.y;
+
+    if (dx < 0) dx = 0;
+    if (dx > elRect.width) dx = elRect.width;
+    if (dy < 0) dy = 0;
+    if (dy > elRect.height) dy = elRect.height;
+
+    position.x = toNumber(dx / elRect.width, { decimal: 2 });
+    position.y = toNumber(dy / elRect.height, { decimal: 2 });
+  };
+
+  const _onmousemove = (event: MouseEvent) => {
+    update(event);
+
+    if (onmousemove) {
+      onmousemove(position, event);
+    }
+  };
+
+  const _onmouseup = (event: MouseEvent) => {
+    document.removeEventListener('mousemove', _onmousemove);
+    document.removeEventListener('mouseup', _onmouseup);
+
+    if (onmouseup) {
+      onmouseup(position, event);
+    }
+  };
+
+  const _onmousedown = (event: MouseEvent) => {
+    // elRect 鍙兘涓嶅噯纭紝杩欓噷鏇存柊涓�涓�
+    elRect = el.getBoundingClientRect();
+
+    update(event);
+
+    document.addEventListener('mousemove', _onmousemove);
+    document.addEventListener('mouseup', _onmouseup);
+
+    if (onmousedown) {
+      onmousedown(position, event);
+    }
+  };
+
+  el.addEventListener('mousedown', _onmousedown);
+
+  return {
+    destroy() {
+      el.removeEventListener('mousedown', _onmousedown);
+    },
+  };
+};
diff --git a/src/fabric-editor/components/color-picker/utils/tool.ts b/src/fabric-editor/components/color-picker/utils/tool.ts
new file mode 100644
index 0000000..6c20dd4
--- /dev/null
+++ b/src/fabric-editor/components/color-picker/utils/tool.ts
@@ -0,0 +1,6 @@
+export const toNumber = (n: number, { decimal = 0 } = {}) => {
+  if (decimal > 0) {
+    return Number(n.toFixed(decimal));
+  }
+  return Math.round(n);
+};
diff --git a/src/fabric-editor/components/colorSelector.vue b/src/fabric-editor/components/colorSelector.vue
new file mode 100644
index 0000000..a484e85
--- /dev/null
+++ b/src/fabric-editor/components/colorSelector.vue
@@ -0,0 +1,278 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2023-02-16 22:52:00
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-21 15:15:04
+ * @Description: 棰滆壊閫夋嫨鍣�
+-->
+<template>
+  <div class="box">
+    <!-- 棰滆壊寮�鍏� -->
+    <iSwitch v-model="isGradient" size="large" class="switch">
+      <template #open>
+        <span>娓愬彉</span>
+      </template>
+      <template #close>
+        <span>绾壊</span>
+      </template>
+    </iSwitch>
+    <!-- 娓愬彉 -->
+    <div v-if="isGradient">
+      <div class="gradient-bar" :style="bgStr"></div>
+      <!-- 棰滆壊鎻掍欢 -->
+
+      <gradientColorPicker
+        :is-gradient="true"
+        :gradient="currentGradient"
+        @change="changeGradientColor"
+        :cancel-text="$t('cancel')"
+        :confirm-text="$t('ok')"
+      />
+    </div>
+
+    <!-- 绾壊閫夋嫨鍣� -->
+    <ColorPicker v-show="!isGradient" v-model="fill" @on-change="changePureColor" alpha />
+  </div>
+</template>
+
+<script setup name="ColorSelector">
+// import 'color-gradient-picker-vue3/dist/style.css';
+// import gradientColorPicker from 'color-gradient-picker-vue3';
+import { fabric } from 'fabric';
+import useSelect from '@/hooks/select';
+import { debounce } from 'lodash-es';
+const { canvasEditor } = useSelect();
+const generateFabricGradientFromColorStops = (handlers, width, height, orientation, angle) => {
+  // 瑙掑害杞崲鍧愭爣
+  const gradAngleToCoords = (paramsAngle) => {
+    const anglePI = -parseInt(paramsAngle, 10) * (Math.PI / 180);
+    return {
+      x1: Math.round(50 + Math.sin(anglePI) * 50) / 100,
+      y1: Math.round(50 + Math.cos(anglePI) * 50) / 100,
+      x2: Math.round(50 + Math.sin(anglePI + Math.PI) * 50) / 100,
+      y2: Math.round(50 + Math.cos(anglePI + Math.PI) * 50) / 100,
+    };
+  };
+
+  // 鐢熸垚绾挎�ф笎鍙�
+  const generateLinear = (colorStops) => {
+    const angleCoords = gradAngleToCoords(angle);
+    return new fabric.Gradient({
+      type: 'linear',
+      coords: {
+        x1: angleCoords.x1 * width,
+        y1: angleCoords.y1 * height,
+        x2: angleCoords.x2 * width,
+        y2: angleCoords.y2 * height,
+      },
+      colorStops,
+    });
+  };
+
+  // 鐢熸垚寰勫悜娓愬彉
+  const generateRadial = (colorStops) => {
+    return new fabric.Gradient({
+      type: 'radial',
+      coords: {
+        x1: width / 2,
+        y1: height / 2,
+        r1: 0,
+        x2: width / 2,
+        y2: height / 2,
+        r2: width / 2,
+      },
+      colorStops,
+    });
+  };
+
+  let bgGradient = {};
+  const colorStops = [...handlers];
+  if (orientation === 'linear') {
+    bgGradient = generateLinear(colorStops);
+  } else if (orientation === 'radial') {
+    bgGradient = generateRadial(colorStops);
+  }
+
+  return bgGradient;
+};
+const props = defineProps({
+  angleKey: {
+    type: String,
+    default: 'gradientAngle',
+  },
+  color: {
+    type: [Object, String],
+    default: '',
+  },
+});
+const emitChange = defineEmits(['change']);
+// const poptipCreated = ref(false);
+// 鏄惁娓愬彉
+const isGradient = ref(false);
+// 绾壊
+const fill = ref('');
+// 娓愬彉
+const bgStr = ref('background: linear-gradient(124deg, rgb(28, 27, 27) 0%, rgb(255, 0, 0) 100%);');
+const currentGradient = reactive({
+  type: 'linear',
+  degree: 0,
+  points: [
+    {
+      left: 0,
+      red: 0,
+      green: 0,
+      blue: 0,
+      alpha: 1,
+    },
+    {
+      left: 100,
+      red: 255,
+      green: 0,
+      blue: 0,
+      alpha: 1,
+    },
+  ],
+});
+// const onPoptipCreated = () => {
+// poptipCreated.value = true;
+// };
+// 鍥炴樉棰滆壊
+const checkColor = (val) => {
+  if (typeof val === 'string') {
+    isGradient.value = false;
+    fill.value = val;
+  } else {
+    // 娓愬彉
+    isGradient.value = true;
+    const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+    if (activeObject) {
+      // 鎺т欢灞炴�ц缃�
+      fabricGradientToCss(val, activeObject);
+      // bar鑳屾櫙璁剧疆
+      fabricGradientToBar(val);
+    }
+  }
+};
+const changeGradientColor = debounce(function (val) {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  const { gradient } = val;
+  if (activeObject) {
+    const currentGradient = cssToFabricGradient(gradient, activeObject);
+    // TODO:
+    emitChange('change', currentGradient);
+
+    // 淇濆瓨瑙掑害锛岀敤浜庝笅涓�娆¢�変腑灞曠ず
+    activeObject.set(props.angleKey, gradient.degree);
+    setGradientBar(val);
+  }
+}, 500);
+// 璁剧疆娓愬彉棰滆壊鏉�
+const setGradientBar = (val) => {
+  if (val.gradient.type === 'linear') {
+    bgStr.value = `background: ${val.style};`;
+  } else {
+    bgStr.value = `background: ${val.style.replace('radial', 'linear')};`;
+  }
+};
+// Fabric娓愬彉bar鑳屾櫙璁剧疆
+const fabricGradientToBar = (val) => {
+  // 鐧惧垎姣旀帓搴�
+  if (!val?.colorStops) return; // 闃叉浠庢ā鏉垮姞杞藉悗鍑虹幇colorStops鎶ラ敊
+  val.colorStops.sort((a, b) => a.offset - b.offset);
+  const str = val.colorStops.map((item) => `${item.color} ${item.offset * 100}%`);
+  bgStr.value = `background: linear-gradient(124deg, ${str});`;
+};
+// Fabric娓愬彉杞琧ss
+const fabricGradientToCss = (val, activeObject) => {
+  // 娓愬彉绫诲瀷
+  if (!val) return;
+  currentGradient.type = val.type;
+  currentGradient.degree = activeObject.get(props.angleKey, val.degree);
+  currentGradient.points = val.colorStops.map((item) => {
+    const [red, green, blue, alpha] = item.color.replace(/^rgba?\(|\s+|\)$/g, '').split(',');
+    return {
+      left: item.offset * 100,
+      red: Number(red),
+      green: Number(green),
+      blue: Number(blue),
+      alpha: Number(alpha),
+    };
+  });
+};
+// css杞現abric娓愬彉
+const cssToFabricGradient = (val, activeObject) => {
+  const handlers = val.points.map((item) => ({
+    offset: item.left / 100,
+    color: `rgba(${item.red}, ${item.green}, ${item.blue}, ${item.alpha})`,
+  }));
+  return generateFabricGradientFromColorStops(
+    handlers,
+    activeObject.width,
+    activeObject.height,
+    val.type,
+    val.degree
+  );
+};
+// 绾壊棰滆壊
+const changePureColor = (val) => {
+  emitChange('change', val);
+};
+watch(
+  () => props.color,
+  (val) => {
+    checkColor(val);
+  }
+);
+onMounted(() => {
+  checkColor(props.color);
+});
+</script>
+
+<style scoped lang="less">
+.box {
+  padding: 10px 0;
+}
+
+// 娓愬彉鏉�
+.gradient-bar {
+  width: 100%;
+  height: 30px;
+  cursor: pointer;
+  border-radius: 5px;
+}
+
+.switch {
+  margin-bottom: 10px;
+}
+
+// 鎻愮ず寮规
+:deep(.ivu-color-picker) {
+  display: block;
+}
+
+:deep(.ivu-poptip-body) {
+  padding: 5px;
+}
+
+:deep(.ivu-poptip) {
+  width: 100%;
+
+  .ivu-poptip-rel {
+    width: 100%;
+  }
+}
+
+// 娓愬彉閫夋嫨鍣�
+:deep(.ui-color-picker) {
+  .picker-area,
+  .gradient-controls,
+  .color-preview-area {
+    padding: 0;
+  }
+  border-radius: 10px;
+  padding: 8px;
+  margin: 0;
+  margin-top: 10px;
+  width: 100%;
+}
+</style>
diff --git a/src/fabric-editor/components/common/modalSzie.vue b/src/fabric-editor/components/common/modalSzie.vue
new file mode 100644
index 0000000..1754e16
--- /dev/null
+++ b/src/fabric-editor/components/common/modalSzie.vue
@@ -0,0 +1,100 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-11 19:09:45
+ * @Description: 鍏叡灏哄
+-->
+
+<template>
+  <Modal v-model="modal" :title="props.title" footer-hide>
+    <h3>
+      {{ $t('importFiles.createDesign.customSize') }}
+    </h3>
+    <Form ref="formInline" inline :label-width="40">
+      <FormItem label="瀹藉害">
+        <InputNumber v-model="width" :min="1" placeholder="璇疯緭鍏�"></InputNumber>
+      </FormItem>
+      <FormItem label="楂樺害">
+        <InputNumber v-model="height" :min="1" placeholder="璇疯緭鍏�"></InputNumber>
+      </FormItem>
+      <FormItem :label-width="0">
+        <Button type="primary" @click="customSizeCreate">
+          {{ $t('importFiles.createDesign.create') }}
+        </Button>
+      </FormItem>
+    </Form>
+    <Divider class="divider" />
+    <h3>
+      {{ $t('importFiles.createDesign.systemSize') }}
+    </h3>
+    <CellGroup @on-click="setSize">
+      <Cell
+        :title="item.name"
+        :label="`${item.width}x${item.height}${item.unit}`"
+        :arrow="false"
+        :key="item.name"
+        :name="`${item.width}x${item.height}x${item.unit}`"
+        v-for="item in sizeList"
+      >
+        <template #extra>
+          <Icon type="md-add" />
+        </template>
+      </Cell>
+    </CellGroup>
+  </Modal>
+</template>
+
+<script name="ImportJson" setup>
+import useSelect from '@/hooks/select';
+import { Message } from 'view-ui-plus';
+const { canvasEditor } = useSelect();
+const emit = defineEmits(['set']);
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: '',
+  },
+});
+
+const modal = ref(false);
+const width = ref(null);
+const height = ref(null);
+const sizeList = ref([]);
+const showSetSize = (w, h) => {
+  width.value = w || null;
+  height.value = h || null;
+  // 鑾峰彇绱犳潗
+  canvasEditor.getSizeList().then((res) => {
+    sizeList.value = res;
+  });
+  modal.value = true;
+};
+const setSize = (itemString) => {
+  const [w, h] = itemString.split('x');
+  width.value = Number(w);
+  height.value = Number(h);
+};
+
+const customSizeCreate = async () => {
+  if (width.value && height.value) {
+    emit('set', width.value, height.value);
+    modal.value = false;
+  } else {
+    Message.warning('璇锋鏌ュ昂瀵�');
+  }
+};
+
+defineExpose({
+  showSetSize,
+});
+</script>
+<style scoped lang="less">
+h3 {
+  margin-bottom: 10px;
+}
+.divider {
+  margin-top: 0;
+}
+</style>
diff --git a/src/fabric-editor/components/common/pageList.vue b/src/fabric-editor/components/common/pageList.vue
new file mode 100644
index 0000000..66afd56
--- /dev/null
+++ b/src/fabric-editor/components/common/pageList.vue
@@ -0,0 +1,140 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-11 16:34:23
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-12 15:41:52
+ * @Description: 鍒嗛〉缁勪欢
+-->
+
+<template>
+  <!-- 鍒楄〃 -->
+  <div class="page-list-box" style="height: calc(100vh - 100px)" :id="props.DOMId">
+    <Scroll
+      :key="props.DOMId"
+      v-if="showScroll"
+      :on-reach-bottom="nextPage"
+      :height="scrollHeight"
+      :distance-to-edge="[-1, -1]"
+    >
+      <div class="img-box" v-if="pageData.length">
+        <!-- 鍒楄〃 -->
+        <div class="img-item" v-for="info in pageData" :key="info.id">
+          <Tooltip :content="info.name" placement="top">
+            <Image
+              lazy
+              :src="info.src"
+              @click="(e) => emit('click', { info, e })"
+              @dragend="(e) => emit('dragend', { info, e })"
+              fit="contain"
+              width="100%"
+              height="100%"
+              :alt="info.name"
+            />
+          </Tooltip>
+        </div>
+      </div>
+      <Spin size="large" fix :show="pageLoading"></Spin>
+      <Divider plain v-if="isDownBottom">{{ pageData.length ? '宸茬粡鍒板簳浜�' : '鏆傛棤鍐呭' }}</Divider>
+    </Scroll>
+  </div>
+</template>
+
+<script name="ImportJson" setup>
+import usePageList from '@/hooks/usePageList';
+
+const emit = defineEmits(['back', 'click', 'dragend']);
+
+const props = defineProps({
+  pageListApi: {
+    type: Function,
+  },
+  filters: {
+    type: Object,
+  },
+  DOMId: {
+    type: String,
+    default: '',
+  },
+  formatData: {
+    type: Function,
+  },
+});
+
+const sort = [];
+
+// 閫氱敤鍒嗛〉
+const {
+  pageData,
+  showScroll,
+  scrollHeight,
+  isDownBottom,
+  pageLoading,
+  startPage,
+  startGetList,
+  nextPage,
+} = usePageList({
+  el: '#' + props.DOMId,
+  apiClient: props.pageListApi,
+  filters: props.filters,
+  sort,
+  fields: [],
+  formatData: props.formatData,
+});
+
+onMounted(async () => {
+  startPage();
+});
+
+defineExpose({
+  startGetList,
+  startPage,
+});
+</script>
+<style scoped lang="less">
+.page-list-box {
+  margin-top: 10px;
+}
+:deep(.ivu-scroll-container) {
+  div.ivu-scroll-loader:first-child {
+    height: 0;
+  }
+}
+:deep(.ivu-divider-horizontal) {
+  &.ivu-divider-with-text-center {
+    margin-bottom: 0;
+  }
+}
+:deep(.ivu-tooltip-rel) {
+  display: block;
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+:deep(.ivu-tooltip) {
+  display: block;
+  height: 100%;
+  width: 100%;
+}
+
+.img-box {
+  display: grid;
+  display: grid;
+  grid-template-columns: repeat(3, 90px);
+  grid-auto-rows: 90px;
+  grid-row-gap: 10px;
+  justify-content: space-between;
+  padding: 8px;
+  background: #f1f2f4;
+  border-radius: 10px;
+  margin-bottom: 10px;
+  .img-item {
+    border-radius: 5px;
+    padding: 5px;
+    cursor: pointer;
+    &:hover {
+      background: #bababa;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/common/searchType.vue b/src/fabric-editor/components/common/searchType.vue
new file mode 100644
index 0000000..2516b74
--- /dev/null
+++ b/src/fabric-editor/components/common/searchType.vue
@@ -0,0 +1,121 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-11 16:04:59
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-12 16:37:40
+ * @Description: 鎼滅储缁勪欢
+-->
+
+<template>
+  <div class="search-box">
+    <Button
+      type="text"
+      class="back-btn"
+      @click="clear"
+      v-if="typeValue || searchKeyWord"
+      icon="ios-arrow-back"
+    ></Button>
+
+    <Select class="select" v-model="typeValue" :disabled="loading" @on-change="change">
+      <Option v-for="item in typeList" :value="item.value" :key="item.value">
+        {{ item.label }}
+      </Option>
+    </Select>
+    <Input
+      class="input"
+      :placeholder="`鍦�${typeText}涓悳绱"
+      v-model="searchKeyWord"
+      :disabled="loading"
+      clearable
+      @on-change="inputChange"
+      @on-search="change"
+      search
+    />
+  </div>
+</template>
+
+<script name="ImportJson" setup>
+import { debounce } from 'lodash-es';
+
+const props = defineProps({
+  typeListApi: {
+    type: Function,
+    Object: () => ({}),
+  },
+});
+const emit = defineEmits(['change']);
+
+const loading = ref(false);
+
+const typeValue = ref('');
+const searchKeyWord = ref('');
+const typeList = ref([]);
+const typeText = computed(() => {
+  const info = typeList.value.find((item) => item.value === typeValue.value);
+  return info?.label || '鍏ㄩ儴';
+});
+
+onMounted(async () => {
+  loading.value = true;
+
+  try {
+    const res = await props.typeListApi();
+    const list = res.data.data.map((item) => {
+      return {
+        value: item.id,
+        label: item.attributes.name,
+      };
+    });
+    typeList.value = [
+      {
+        label: '鍏ㄩ儴',
+        value: '',
+      },
+      ...list,
+    ];
+  } catch (error) {
+    console.log(error);
+  }
+
+  loading.value = false;
+});
+
+const change = () => {
+  emit('change', { searchKeyWord: searchKeyWord.value, typeValue: typeValue.value });
+};
+
+// const getValue = () => {
+//   return { type: typeValue.value, searchKeyWord };
+// };
+const clear = () => {
+  typeValue.value = '';
+  searchKeyWord.value = '';
+  change();
+};
+
+const setType = (type) => {
+  typeValue.value = type;
+};
+
+const inputChange = debounce(change, 300);
+
+defineExpose({
+  setType,
+});
+</script>
+<style scoped lang="less">
+.search-box {
+  padding-top: 10px;
+  display: flex;
+
+  .back-btn {
+    margin-right: 10px;
+  }
+  .input {
+    margin-left: 10px;
+  }
+  .select {
+    width: 100px;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/common/typeList.vue b/src/fabric-editor/components/common/typeList.vue
new file mode 100644
index 0000000..92c3856
--- /dev/null
+++ b/src/fabric-editor/components/common/typeList.vue
@@ -0,0 +1,129 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-11 16:17:17
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-12 15:43:54
+ * @Description: 鍒楄〃缁勪欢
+-->
+
+<template>
+  <div class="material-type">
+    <div class="item" v-for="item in materialTypeList" :key="item.id">
+      <div class="top">
+        <h3>{{ item.name }}</h3>
+        <Button type="text" size="small" @click="emit('selectType', item.id)">鏌ョ湅鏇村</Button>
+      </div>
+      <div class="img-box">
+        <div class="img-item" v-for="info in item.list" :key="info.id">
+          <Image
+            lazy
+            :src="info.src"
+            @click="(e) => emit('click', { info, e })"
+            @dragend="(e) => emit('dragend', { info, e })"
+            fit="contain"
+            width="100%"
+            height="100%"
+            :alt="info.name"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script name="ImportJson" setup>
+const baseURL = import.meta.env.APP_APIHOST;
+
+const emit = defineEmits(['click', 'dragend']);
+
+const props = defineProps({
+  typeApi: {
+    type: Function,
+  },
+  typeListApi: {
+    type: Function,
+  },
+  typeKey: {
+    type: String,
+  },
+  formatData: {
+    type: Function,
+  },
+});
+
+// 绱犳潗鍒嗙被
+const materialTypeList = ref([]);
+const getMaterialTypesHandler = async () => {
+  const res = await props.typeApi();
+  materialTypeList.value = res.data.data.map((item) => {
+    return {
+      name: item.attributes.name,
+      id: item.id,
+      list: [],
+    };
+  });
+};
+
+const getMaterialsByTypeHandler = async () => {
+  materialTypeList;
+  let i = 0;
+  for (const item of materialTypeList.value) {
+    const res = await props.typeListApi({
+      populate: {
+        img: '*',
+      },
+      filters: {
+        [props.typeKey]: {
+          $eq: item.id,
+        },
+      },
+      pagination: {
+        page: 1,
+        pageSize: 8,
+      },
+    });
+    materialTypeList.value[i].list = props.formatData(res.data.data);
+
+    i++;
+  }
+};
+
+onMounted(async () => {
+  await getMaterialTypesHandler();
+  await getMaterialsByTypeHandler();
+});
+</script>
+<style scoped lang="less">
+.item {
+  .top {
+    display: flex;
+    width: 100%;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 0;
+  }
+}
+
+.img-box {
+  background: #f1f2f4;
+  display: grid;
+  padding: 8px;
+  display: grid;
+  grid-template-columns: repeat(4, 60px);
+  grid-template-rows: repeat(2, 70px);
+  grid-row-gap: 10px;
+  justify-content: space-between;
+  padding: 8px;
+  background: #f1f2f4;
+  border-radius: 10px;
+  margin-bottom: 10px;
+  .img-item {
+    border-radius: 5px;
+    padding: 5px;
+    cursor: pointer;
+    &:hover {
+      background: #bababa;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/cropperDialog.vue b/src/fabric-editor/components/cropperDialog.vue
new file mode 100644
index 0000000..826ae03
--- /dev/null
+++ b/src/fabric-editor/components/cropperDialog.vue
@@ -0,0 +1,290 @@
+<template>
+  <Modal
+    v-model="visible"
+    @on-ok="onOk"
+    @on-cancel="onCancel"
+    title="鍥剧墖瑁佸壀"
+    width="80%"
+    style="height: 80%"
+  >
+    <div class="main">
+      <Spin size="large" fix :show="loading">鍥剧墖鍔犺浇涓�...</Spin>
+      <div class="options">
+        <label>瑁佸壀姣斾緥</label>
+        <div class="flex mt-2 ratio-item-wrapper">
+          <div class="ratio-item" :class="{ active: !fixed }" @click="changeRatio()">鑷敱</div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 1 && fixedRatio[1] === 1 }"
+            @click="changeRatio([1, 1])"
+          >
+            <div class="ratio-item-content" style="height: 70px; width: 70px">1:1</div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 2 && fixedRatio[1] === 3 }"
+            @click="changeRatio([2, 3])"
+          >
+            <div class="ratio-item-content" style="height: 70px; width: calc(70px * 2 / 3)">
+              2:3
+            </div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 3 && fixedRatio[1] === 2 }"
+            @click="changeRatio([3, 2])"
+          >
+            <div class="ratio-item-content" style="height: calc(70px * 2 / 3); width: 70px">
+              3:2
+            </div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 4 && fixedRatio[1] === 3 }"
+            @click="changeRatio([4, 3])"
+          >
+            <div class="ratio-item-content" style="height: calc(70px * 3 / 4); width: 70px">
+              4:3
+            </div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 3 && fixedRatio[1] === 4 }"
+            @click="changeRatio([3, 4])"
+          >
+            <div class="ratio-item-content" style="width: calc(70px * 3 / 4); height: 70px">
+              3:4
+            </div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 16 && fixedRatio[1] === 9 }"
+            @click="changeRatio([16, 9])"
+          >
+            <div class="ratio-item-content" style="height: calc(70px * 9 / 16); width: 70px">
+              16:9
+            </div>
+          </div>
+          <div
+            class="ratio-item"
+            :class="{ active: fixed && fixedRatio[0] === 9 && fixedRatio[1] === 16 }"
+            @click="changeRatio([9, 16])"
+          >
+            <div class="ratio-item-content" style="width: calc(70px * 9 / 16); height: 70px">
+              9:16
+            </div>
+          </div>
+        </div>
+        <label>褰撳墠灏哄</label>
+        <div class="flex mt-2">
+          <Input
+            v-model="cropperWidth"
+            type="number"
+            @change="changeCropperSize('width', Number($event.target.value))"
+          />
+          <span class="mx-2">X</span>
+          <Input
+            v-model="cropperHeight"
+            type="number"
+            @change="changeCropperSize('height', Number($event.target.value))"
+          />
+        </div>
+      </div>
+      <div class="cropper-wrapper">
+        <VueCropper
+          ref="cropperRef"
+          :img="img"
+          :outputSize="1"
+          outputType="png"
+          autoCrop
+          :fixed="fixed"
+          :fixedNumber="fixedRatio"
+          centerBox
+          :fixedBox="false"
+          full
+          @realTime="onPreview"
+        />
+      </div>
+      <div class="preview-wrapper">
+        <div
+          style="height: 100px; width: 100px; border: 1px solid rgba(59, 130, 246, 0.5)"
+          :style="previewStyle"
+        >
+          <div :style="previews.div" v-if="previews">
+            <img :src="previews.url" :style="{ ...previews.img, maxWidth: previews.img.width }" />
+          </div>
+        </div>
+
+        <div class="title">棰勮</div>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import 'vue-cropper/dist/index.css';
+import { VueCropper } from 'vue-cropper';
+// import { blobToFile } from '../../views/creation/createShortVideo/components/framesContent/utils'
+
+const visible = ref(false);
+
+const cropperRef = ref();
+
+const img = ref();
+
+const previewStyle = ref({
+  width: '100px',
+  height: '100px',
+  overflow: 'hidden',
+  margin: '0',
+});
+
+const loading = ref(false);
+
+const previews = ref();
+
+const cropperWidth = ref(0);
+const cropperHeight = ref(0);
+
+function changeCropperSize(type: 'width' | 'height', value: number) {
+  if (fixed.value) {
+    if (type === 'width') {
+      cropperHeight.value = (value * fixedRatio.value[1]) / fixedRatio.value[0];
+    } else {
+      cropperWidth.value = (value * fixedRatio.value[0]) / fixedRatio.value[1];
+    }
+  }
+  nextTick(() => {
+    cropperRef.value.goAutoCrop(Number(cropperWidth.value), Number(cropperHeight.value));
+  });
+}
+
+function onPreview(_previews) {
+  if (_previews.w === 0 && _previews.h === 0) {
+    loading.value = true;
+  } else {
+    loading.value = false;
+  }
+  console.log('馃殌 ~ onPreview ~ _previews:', _previews);
+  cropperWidth.value = _previews.w;
+  cropperHeight.value = _previews.h;
+  previewStyle.value = {
+    width: _previews.w + 'px',
+    height: _previews.h + 'px',
+    overflow: 'hidden',
+    margin: '0',
+    zoom: 100 / _previews.w,
+  };
+
+  previews.value = _previews;
+}
+
+function onCancel() {
+  visible.value = false;
+}
+const fixedRatio = ref([1, 1]);
+const fixed = ref(false);
+function changeRatio(ratio?: [number, number]) {
+  if (ratio) {
+    fixed.value = true;
+    fixedRatio.value = ratio;
+    // 瀹藉害涓嶅彉锛屾寜鐓ф瘮渚嬩慨鏀归珮搴�
+    // cropperHeight.value = (cropperWidth.value * ratio[1]) / ratio[0];
+  } else {
+    fixed.value = false;
+  }
+  nextTick(() => {
+    cropperRef.value.goAutoCrop(9999, 9999);
+  });
+}
+
+let _callback = null;
+function onOk() {
+  cropperRef.value.getCropData((data) => {
+    _callback && _callback(data);
+    visible.value = false;
+  });
+}
+
+defineExpose({
+  open(data, callback) {
+    img.value = data.img;
+    _callback = callback;
+    visible.value = true;
+  },
+});
+</script>
+
+<style scoped lang="less">
+.main {
+  display: flex;
+  align-items: stretch;
+  min-height: 600px;
+  position: relative;
+}
+
+.options {
+  width: 256px;
+  .mt-2 {
+    margin-top: 8px;
+  }
+  .flex {
+    display: flex;
+    align-items: center;
+  }
+  .mx-2 {
+    margin: 0 8px;
+  }
+}
+
+.cropper-wrapper {
+  flex: 1;
+  min-height: 100%;
+  padding: 0 16px;
+}
+
+.title {
+  text-align: center;
+  font-size: 14px;
+  color: #0a0a15;
+  margin-top: 12px;
+}
+
+.ratio-item-wrapper {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.ratio-item {
+  width: 80px;
+  height: 80px;
+  // border: 1px solid #99999f;
+  background-color: #fcfcfc;
+  border-radius: 4px;
+  text-align: center;
+  line-height: 30px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+
+  &.active {
+    border: 1px solid #2d8cf0;
+  }
+}
+
+.ratio-item-content {
+  background-color: #f0f0f0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+}
+
+.preview-wrapper {
+  width: 100px;
+}
+</style>
diff --git a/src/fabric-editor/components/cropperImg.vue b/src/fabric-editor/components/cropperImg.vue
new file mode 100644
index 0000000..2aa5d80
--- /dev/null
+++ b/src/fabric-editor/components/cropperImg.vue
@@ -0,0 +1,89 @@
+<template>
+  <div v-if="isOne && type === 'image'" class="attr-item-box">
+    <div class="bg-item">
+      <Button @click="cropper" type="text" long>{{ $t('cropperImg') }}</Button>
+    </div>
+  </div>
+  <cropperDialog ref="cropperDialogRef"></cropperDialog>
+</template>
+
+<script setup name="CropperImg">
+import useSelect from '@/hooks/select';
+
+import cropperDialog from '@/components/cropperDialog.vue';
+import { Utils } from '@kuaitu/core';
+const { insertImgFile } = Utils;
+
+const update = getCurrentInstance();
+// const canvasEditor = inject('canvasEditor');
+const { canvasEditor, isOne } = useSelect();
+const type = ref('');
+const cropperDialogRef = ref();
+const cropper = () => {
+  console.log('馃殌 ~ cropper ~ cropper:');
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject && activeObject.type === 'image') {
+    console.log('馃殌 ~ cropper ~ activeObject:', activeObject);
+    cropperDialogRef.value.open({ img: activeObject._element.src }, async (data) => {
+      console.log('馃殌 ~ cropper ~ data:', data);
+      const imgEl = await insertImgFile(data);
+      // const width = activeObject.get('width');
+      // const height = activeObject.get('height');
+      // const scaleX = activeObject.get('scaleX');
+      // const scaleY = activeObject.get('scaleY');
+      // console.log('馃殌 ~ cropper ~ scaleX:', scaleX);
+      // console.log('馃殌 ~ cropper ~ scaleY:', scaleY);
+      activeObject.setSrc(imgEl.src, () => {
+        // activeObject.set('scaleX', scaleX);
+        // activeObject.set('scaleY', scaleY);
+        canvasEditor.canvas.renderAll();
+      });
+      imgEl.remove();
+    });
+  }
+};
+
+// 鏇挎崲鍥剧墖
+// const repleace = async () => {
+//   const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+//   if (activeObject && activeObject.type === 'image') {
+//     // 鍥剧墖
+//     const [file] = await selectFiles({ accept: 'image/*', multiple: false });
+//     // 杞瓧绗︿覆
+//     const fileStr = await getImgStr(file);
+//     // 瀛楃涓茶浆El
+//     const imgEl = await insertImgFile(fileStr);
+//     const width = activeObject.get('width');
+//     const height = activeObject.get('height');
+//     const scaleX = activeObject.get('scaleX');
+//     const scaleY = activeObject.get('scaleY');
+//     activeObject.setSrc(imgEl.src, () => {
+//       activeObject.set('scaleX', (width * scaleX) / imgEl.width);
+//       activeObject.set('scaleY', (height * scaleY) / imgEl.height);
+//       canvasEditor.canvas.renderAll();
+//     });
+//     imgEl.remove();
+//   }
+// };
+
+const init = () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    type.value = activeObject.type;
+    update?.proxy?.$forceUpdate();
+  }
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', init);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', init);
+});
+</script>
+<style lang="less" scoped>
+.attr-item-box {
+  margin-top: 8px;
+}
+</style>
diff --git a/src/fabric-editor/components/del.vue b/src/fabric-editor/components/del.vue
new file mode 100644
index 0000000..6386a4e
--- /dev/null
+++ b/src/fabric-editor/components/del.vue
@@ -0,0 +1,33 @@
+<template>
+  <el-tooltip v-if="isSelect && isMatchType" :content="'鍒犻櫎'">
+    <el-button @click="del" icon="Delete" text></el-button>
+  </el-tooltip>
+</template>
+
+<script setup name="Del">
+import useSelect from '@/fabric-editor/hooks/select';
+import { debounce } from 'lodash';
+import { TemplateParamObjectName } from '@/fabric-editor/customObject';
+
+// 鍙慨鏀圭殑鍏冪礌
+const baseType = [
+  'text',
+  'i-text',
+  'textbox',
+  'rect',
+  'circle',
+  'triangle',
+  'polygon',
+  // 'image',
+  'group',
+  'line',
+  'arrow',
+  'thinTailArrow',
+  TemplateParamObjectName,
+];
+const { isMatchType, canvasEditor, isSelect, isOne } = useSelect(baseType);
+
+const del = debounce(function () {
+  canvasEditor.del();
+}, 300);
+</script>
diff --git a/src/fabric-editor/components/dragMode.vue b/src/fabric-editor/components/dragMode.vue
new file mode 100644
index 0000000..ffbd04b
--- /dev/null
+++ b/src/fabric-editor/components/dragMode.vue
@@ -0,0 +1,48 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2023-04-18 08:06:56
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-04-24 12:07:49
+ * @Description: 鎷栨嫿妯″紡
+-->
+
+<template>
+  <div class="box">
+    <Switch size="large" v-model="status" @on-change="switchMode">
+      <template #open>
+        <span>Drag</span>
+      </template>
+    </Switch>
+  </div>
+</template>
+
+<script setup name="Drag">
+import useSelect from '@/hooks/select';
+const status = ref(false);
+const { canvasEditor } = useSelect();
+
+const switchMode = (val) => {
+  if (val) {
+    canvasEditor.startDring();
+  } else {
+    canvasEditor.endDring();
+  }
+};
+
+onMounted(() => {
+  canvasEditor.on('startDring', () => (status.value = true));
+  canvasEditor.on('endDring', () => (status.value = false));
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('startDring');
+  canvasEditor.off('endDring');
+});
+</script>
+<style scoped lang="less">
+.box {
+  position: absolute;
+  right: 193px;
+  bottom: 14px;
+}
+</style>
diff --git a/src/fabric-editor/components/edit.vue b/src/fabric-editor/components/edit.vue
new file mode 100644
index 0000000..ffffa6e
--- /dev/null
+++ b/src/fabric-editor/components/edit.vue
@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import useSelect from '@/hooks/select';
+const { isMatchType, canvasEditor } = useSelect(['polygon']);
+import { Message } from 'view-ui-plus';
+const onEditPolygon = () => {
+  const obj = canvasEditor.fabricCanvas?.getActiveObject();
+  if (obj && obj.type === 'polygon') {
+    canvasEditor.activeEdit();
+  } else {
+    Message.warning('璇锋鏌ラ�夋嫨polygon');
+  }
+};
+</script>
+
+<template>
+  <Tooltip :content="$t('quick.editPoly')" v-if="isMatchType">
+    <Button long @click="onEditPolygon" icon="md-brush" type="text"></Button>
+  </Tooltip>
+</template>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/filters.vue b/src/fabric-editor/components/filters.vue
new file mode 100644
index 0000000..7740c82
--- /dev/null
+++ b/src/fabric-editor/components/filters.vue
@@ -0,0 +1,310 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2023-04-06 23:04:38
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:19:51
+ * @Description: 鍥剧墖婊ら暅
+-->
+
+<template>
+  <div v-if="isOne && state.type === 'image'" class="box">
+    <Divider plain orientation="left">
+      <h4>鍥剧墖婊ら暅</h4>
+    </Divider>
+    <Collapse>
+      <Panel name="1">
+        {{ $t('filters.simple') }}
+        <template #content>
+          <div class="filter-box">
+            <!-- 鏃犲弬鏁版护闀� -->
+            <div class="filter-item" v-for="(value, key) in state.noParamsFilters" :key="key">
+              <img
+                :src="getImageUrl(key)"
+                alt=""
+                @click="changeFilters(key, !noParamsFilters[key])"
+              />
+              <Checkbox
+                v-model="state.noParamsFilters[key]"
+                @on-change="(val) => changeFilters(key, val)"
+              >
+                {{ $t('filters.' + key) }}
+              </Checkbox>
+            </div>
+          </div>
+        </template>
+      </Panel>
+      <Panel name="2">
+        {{ $t('filters.complex') }}
+        <template #content>
+          <!-- 鏈夊弬鏁版护闀滀笌缁勫悎鍙傛暟婊ら暅 -->
+          <div>
+            <div
+              class="filter-item has-params"
+              v-for="item in [...state.paramsFilters, ...state.combinationFilters]"
+              :key="item.type"
+            >
+              <Checkbox v-model="item.status" @on-change="changeFiltersByParams(item.type)">
+                {{ $t('filters.' + item.type) }}
+              </Checkbox>
+              <div v-if="item.status" class="content">
+                <div class="content slider-box" v-for="info in item.params" :key="info">
+                  <div v-if="info.uiType === uiType.SELECT">
+                    <RadioGroup v-model="info.value" @on-change="changeFiltersByParams(item.type)">
+                      <Radio :label="listItem" v-for="listItem in info.list" :key="listItem">
+                        {{ $t('filters.' + item.type + 'List.' + listItem) }}
+                      </Radio>
+                    </RadioGroup>
+                  </div>
+                  <div v-if="info.uiType === uiType.NUMBER">
+                    <Slider
+                      v-model="info.value"
+                      :max="info.max"
+                      :min="info.min"
+                      :step="info.step"
+                      @on-input="changeFiltersByParams(item.type)"
+                    ></Slider>
+                  </div>
+                  <div v-if="info.uiType === uiType.COLOR">
+                    <ColorPicker
+                      v-model="info.value"
+                      alpha
+                      size="small"
+                      @on-change="changeFiltersByParams(item.type)"
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+      </Panel>
+    </Collapse>
+  </div>
+</template>
+
+<script name="Filter" setup>
+import useSelect from '@/hooks/select';
+import { uiType, paramsFilters, combinationFilters } from '@/config/constants/filter';
+
+const { fabric, isOne, canvasEditor } = useSelect();
+const update = getCurrentInstance();
+// 鏃犲弬鏁版护闀�
+const noParamsFilters = {
+  BlackWhite: false,
+  Brownie: false,
+  Vintage: false,
+  Kodachrome: false,
+  technicolor: false,
+  Polaroid: false,
+  Invert: false,
+  Sepia: false,
+};
+
+const state = reactive({
+  uiType,
+  noParamsFilters,
+  paramsFilters: [...paramsFilters],
+  combinationFilters: [...combinationFilters],
+  type: '',
+});
+
+// 鏃犲弬鏁版护闀滀慨鏀圭姸鎬�
+const changeFilters = (type, value) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  state.noParamsFilters[type] = value;
+  if (value) {
+    const itemFilter = _getFilter(activeObject, type);
+    if (!itemFilter) {
+      _createFilter(activeObject, type);
+    }
+  } else {
+    _removeFilter(activeObject, type);
+  }
+};
+// 鏈夊弬鏁颁笌缁勫悎婊ら暅淇敼
+const changeFiltersByParams = (type) => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  const filtersAll = [...state.paramsFilters, ...state.combinationFilters];
+  const moduleInfo = filtersAll.find((item) => item.type === type);
+  if (moduleInfo.status) {
+    // 缁勫悎鍙傛暟婊ら暅淇敼
+    if (moduleInfo.handler) {
+      _changeAttrByHandler(moduleInfo);
+    } else {
+      // 鏈夊弬鏁版护闀滀慨鏀�
+      moduleInfo.params.forEach((paramsItem) => {
+        _changeAttr(type, paramsItem.key, paramsItem.value);
+      });
+    }
+  } else {
+    _removeFilter(activeObject, type);
+  }
+};
+
+const handleSelectOne = () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    state.type = activeObject.type;
+    if (state.type === 'image') {
+      // 鏃犲弬鏁版护闀滃洖鏄�
+      Object.keys(noParamsFilters).forEach((type) => {
+        state.noParamsFilters[type] = !!_getFilter(activeObject, type);
+        update?.proxy?.$forceUpdate();
+      });
+      // 鏈夊弬鏁版护闀滃洖鏄�
+      paramsFilters.forEach((filterItem) => {
+        const moduleInfo = state.paramsFilters.find((item) => item.type === filterItem.type);
+        const filterInfo = _getFilter(activeObject, filterItem.type);
+        moduleInfo.status = !!filterInfo;
+        moduleInfo.params.forEach((paramsItem) => {
+          paramsItem.value = filterInfo ? filterInfo[paramsItem.key] : paramsItem.value;
+        });
+      });
+
+      // 缁勫悎婊ら暅鍥炴樉
+      combinationFilters.forEach((filterItem) => {
+        const moduleInfo = state.combinationFilters.find((item) => item.type === filterItem.type);
+        const filterInfo = _getFilter(activeObject, filterItem.type);
+        moduleInfo.status = !!filterInfo;
+        // 涓嶅洖鏄惧叿浣撳弬鏁�
+      });
+    }
+    update?.proxy?.$forceUpdate();
+  }
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', handleSelectOne);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', handleSelectOne);
+});
+
+// 鍥剧墖鍦板潃鎷兼帴
+function getImageUrl(name) {
+  return new URL(`../assets/filters/${name}.png`, import.meta.url).href;
+}
+
+// 璁剧疆婊ら暅鍊�
+function _changeAttr(type, key, value) {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  const itemFilter = _getFilter(activeObject, type);
+  if (itemFilter) {
+    itemFilter[key] = value;
+  } else {
+    const imgFilter = _createFilter(activeObject, type);
+    imgFilter[key] = value;
+  }
+  activeObject.applyFilters();
+  canvasEditor.canvas.renderAll();
+}
+
+function _changeAttrByHandler(moduleInfo) {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  // 鍒犻櫎
+  _removeFilter(activeObject, moduleInfo.type);
+  // 鍒涘缓
+  const params = moduleInfo.params.map((item) => item.value);
+  _createFilter(activeObject, moduleInfo.type, moduleInfo.handler(...params));
+}
+
+/**
+ * Create filter instance
+ * @param {fabric.Image} sourceImg - Source image to apply filter
+ * @param {string} type - Filter type
+ * @param {Object} [options] - Options of filter
+ * @returns {Object} Fabric object of filter
+ * @private
+ */
+function _createFilter(sourceImg, type, options = null) {
+  let filterObj;
+  // capitalize first letter for matching with fabric image filter name
+  const fabricType = _getFabricFilterType(type);
+  const ImageFilter = fabric.Image.filters[fabricType];
+  if (ImageFilter) {
+    filterObj = new ImageFilter(options);
+    filterObj.options = options;
+    sourceImg.filters.push(filterObj);
+  }
+  sourceImg.applyFilters();
+  canvasEditor.canvas.renderAll();
+  return filterObj;
+}
+/**
+ * Get applied filter instance
+ * @param {fabric.Image} sourceImg - Source image to apply filter
+ * @param {string} type - Filter type
+ * @returns {Object} Fabric object of filter
+ * @private
+ */
+function _getFilter(sourceImg, type) {
+  let imgFilter = null;
+
+  if (sourceImg) {
+    const fabricType = _getFabricFilterType(type);
+    const { length } = sourceImg.filters;
+    let item, i;
+
+    for (i = 0; i < length; i += 1) {
+      item = sourceImg.filters[i];
+      if (item.type === fabricType) {
+        imgFilter = item;
+        break;
+      }
+    }
+  }
+
+  return imgFilter;
+}
+/**
+ * Remove applied filter instance
+ * @param {fabric.Image} sourceImg - Source image to apply filter
+ * @param {string} type - Filter type
+ * @private
+ */
+function _removeFilter(sourceImg, type) {
+  const fabricType = _getFabricFilterType(type);
+  sourceImg.filters = sourceImg.filters.filter((value) => value.type !== fabricType);
+  sourceImg.applyFilters();
+  canvasEditor.canvas.renderAll();
+}
+/**
+ * Change filter class name to fabric's, especially capitalizing first letter
+ * @param {string} type - Filter type
+ * @example
+ * 'grayscale' -> 'Grayscale'
+ * @returns {string} Fabric filter class name
+ */
+function _getFabricFilterType(type) {
+  return type.charAt(0).toUpperCase() + type.slice(1);
+}
+</script>
+
+<style scoped lang="less">
+.filter-box {
+  overflow: hidden;
+  .filter-item {
+    float: left;
+    cursor: pointer;
+    width: 50%;
+    margin-bottom: 10px;
+    img {
+      width: 90%;
+      height: auto;
+    }
+  }
+}
+.has-params {
+  display: inline-block;
+  margin-bottom: 10px;
+  width: 50%;
+  .content {
+    width: 90%;
+  }
+  cursor: none;
+}
+.box {
+  margin-bottom: 12px;
+}
+</style>
diff --git a/src/fabric-editor/components/flip.vue b/src/fabric-editor/components/flip.vue
new file mode 100644
index 0000000..9515342
--- /dev/null
+++ b/src/fabric-editor/components/flip.vue
@@ -0,0 +1,58 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-13 17:14:52
+ * @Description: 鍏冪礌缈昏浆
+-->
+
+<template>
+  <div v-if="isOne" class="attr-item-box">
+    <!-- <h3>{{ $t('attrSeting.flip.name') }}</h3> -->
+    <!-- <Divider plain orientation="left">
+      <h4>{{ $t('attrSeting.flip.name') }}</h4>
+    </Divider> -->
+    <div class="bg-item">
+      <Tooltip :content="$t('attrSeting.flip.x')">
+        <Button long @click="flip('X')" type="text">
+          <flipX width="14" height="14"></flipX>
+        </Button>
+      </Tooltip>
+      <Tooltip :content="$t('attrSeting.flip.y')">
+        <Button long @click="flip('Y')" type="text">
+          <flipY width="14" height="14"></flipY>
+        </Button>
+      </Tooltip>
+    </div>
+
+    <!-- <Divider plain></Divider> -->
+  </div>
+</template>
+
+<script setup name="Flip">
+import useSelect from '@/hooks/select';
+import flipX from '@/assets/icon/flip/x.svg';
+import flipY from '@/assets/icon/flip/y.svg';
+
+const { isOne, canvasEditor } = useSelect();
+
+const flip = (type) => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  activeObject.set(`flip${type}`, !activeObject[`flip${type}`]).setCoords();
+  canvasEditor.canvas.requestRenderAll();
+};
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-btn) {
+  &[disabled] {
+    svg {
+      opacity: 0.2;
+    }
+  }
+}
+
+.attr-item-box {
+  margin-top: 8px;
+}
+</style>
diff --git a/src/fabric-editor/components/fontStyle.vue b/src/fabric-editor/components/fontStyle.vue
new file mode 100644
index 0000000..5276726
--- /dev/null
+++ b/src/fabric-editor/components/fontStyle.vue
@@ -0,0 +1,136 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2023-08-05 17:47:35
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-07-06 16:55:25
+ * @Description: 瀛椾綋鏍峰紡
+-->
+
+<template>
+  <div>
+    <!-- 鎼滅储缁勪欢 -->
+    <searchType
+      ref="selectTypeRef"
+      :typeListApi="getFontStyleTypes"
+      @change="searchChange"
+    ></searchType>
+
+    <!-- 鍒嗙被鍒楄〃 -->
+    <typeList
+      v-show="!filters.font_style_type.$contains && !filters.name.$contains"
+      :typeApi="getFontStyleTypes"
+      :typeListApi="getFontStyleListByType"
+      typeKey="font_style_type"
+      :formatData="formatData"
+      @selectType="selectType"
+      @click="addItem"
+      @dragend="dragItem"
+    ></typeList>
+
+    <!-- 鎼滅储鍒嗛〉鍒楄〃 -->
+    <pageList
+      v-if="filters.font_style_type.$contains || filters.name.$contains"
+      DOMId="fontMaterialList"
+      :pageListApi="getFontStyles"
+      :filters="filters"
+      :formatData="formatData"
+      @click="addItem"
+      @dragend="dragItem"
+    ></pageList>
+  </div>
+</template>
+
+<script setup name="ImportSvg">
+import searchType from '@/components/common/searchType';
+import typeList from '@/components/common/typeList.vue';
+import pageList from '@/components/common/pageList.vue';
+import useSelect from '@/hooks/select';
+import useCalculate from '@/hooks/useCalculate';
+import { getMaterialInfoUrl, getMaterialPreviewUrl } from '@/hooks/usePageList';
+import { getFontStyleTypes, getFontStyleListByType, getFontStyles } from '@/api/material';
+import { fabric } from 'fabric';
+import { v4 as uuid } from 'uuid';
+import { Spin } from 'view-ui-plus';
+import { useI18n } from 'vue-i18n';
+const { t } = useI18n();
+
+const { canvasEditor } = useSelect();
+
+const { isOutsideCanvas } = useCalculate();
+
+// 鎸夌収绫诲瀷娓叉煋
+const dragItem = async ({ e, info: item }) => {
+  if (isOutsideCanvas(e.clientX, e.clientY)) return;
+  Spin.show({
+    render: (h) => h('div', t('alert.loading_data')),
+  });
+  await canvasEditor.downFontByJSON(JSON.stringify(item.json));
+  const el = JSON.parse(JSON.stringify(item.json));
+  const elType = capitalizeFirstLetter(el.type);
+  new fabric[elType].fromObject(el, (fabricEl) => {
+    canvasEditor.addBaseType(fabricEl, { event: e });
+    Spin.hide();
+  });
+};
+
+const addItem = async ({ info: item }) => {
+  Spin.show({
+    render: (h) => h('div', t('alert.loading_data')),
+  });
+  await canvasEditor.downFontByJSON(JSON.stringify(item.json));
+  const el = JSON.parse(JSON.stringify(item.json));
+  el.id = uuid();
+  const elType = capitalizeFirstLetter(el.type);
+  new fabric[elType].fromObject(el, (fabricEl) => {
+    canvasEditor.addBaseType(fabricEl);
+    Spin.hide();
+  });
+};
+
+function capitalizeFirstLetter(string) {
+  return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+const selectTypeRef = ref();
+
+const filters = reactive({
+  font_style_type: {
+    $contains: '',
+  },
+  name: {
+    $contains: '',
+  },
+});
+
+// 鍒嗛〉鏍煎紡鍖�
+const formatData = (data) => {
+  return data.map((item) => {
+    return {
+      id: item.id,
+      name: item.attributes.name,
+      desc: item.attributes.desc,
+      json: item.attributes.json,
+      src: getMaterialInfoUrl(item.attributes.img),
+      previewSrc: getMaterialPreviewUrl(item.attributes.img),
+    };
+  });
+};
+
+// 鎼滅储鏀瑰彉
+const searchChange = async ({ searchKeyWord, typeValue }) => {
+  filters.name.$contains = '';
+  filters.font_style_type.$contains = '';
+  await nextTick();
+  filters.name.$contains = searchKeyWord;
+  filters.font_style_type.$contains = typeValue;
+};
+
+// 鍒嗙被鍒楄〃閫夋嫨
+const selectType = async (type) => {
+  filters.font_style_type.$contains = type;
+  selectTypeRef.value.setType(type);
+  await nextTick();
+};
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/group.vue b/src/fabric-editor/components/group.vue
new file mode 100644
index 0000000..29594c1
--- /dev/null
+++ b/src/fabric-editor/components/group.vue
@@ -0,0 +1,43 @@
+<template>
+  <div v-if="isMultiple || isGroup" class="attr-item-box">
+    <div class="bg-item">
+      <!-- 缁勫悎鎸夐挳 澶氶�夋椂涓嶅彲鐢� -->
+      <el-button v-if="isMultiple" long :disabled="!isMultiple" @click="group" text>
+        <groupIcon width="14" height="14"></groupIcon>
+        {{ '鎴愮粍' }}
+      </el-button>
+      <!-- 鎷嗗垎缁勫悎鎸夐挳锛屼负鍗曢�変笖缁勫厓绱犳椂鍙敤 -->
+      <el-button v-if="isGroup" long :disabled="!isGroup" @click="unGroup" text>
+        <unGroupIcon width="14" height="14"></unGroupIcon>
+        {{ '鎷嗗垎缁�' }}
+      </el-button>
+    </div>
+
+    <!-- <Divider plain v-if="isGroup"></Divider> -->
+  </div>
+</template>
+
+<script setup name="Group">
+import useSelect from '@/fabric-editor/hooks/select';
+import groupIcon from '@/fabric-editor/assets/icon/group/group.svg';
+import unGroupIcon from '@/fabric-editor/assets/icon/group/unGroup.svg';
+
+const { isGroup, isMultiple, canvasEditor } = useSelect();
+
+// 鎷嗗垎缁�
+const unGroup = () => {
+  canvasEditor.unGroup();
+};
+const group = () => {
+  canvasEditor.group();
+};
+</script>
+<style scoped lang="scss">
+:deep(.ivu-btn) {
+  &[disabled] {
+    svg {
+      opacity: 0.2;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/hide.vue b/src/fabric-editor/components/hide.vue
new file mode 100644
index 0000000..3a6bcd7
--- /dev/null
+++ b/src/fabric-editor/components/hide.vue
@@ -0,0 +1,43 @@
+<!--
+ * @Author: wuchenguang1998
+ * @Date: 2024-05-13 22:34:03
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:20:20
+ * @Description: 闅愯棌鎴栨樉绀哄厓绱�
+-->
+
+<template>
+  <Tooltip :content="$t('quick.hide')" v-if="isOne">
+    <Button long v-if="isHide" @click="doHide(false)" icon="md-eye-off" type="text"></Button>
+    <Button long v-else @click="doHide(true)" icon="md-eye" type="text"></Button>
+  </Tooltip>
+</template>
+
+<script setup name="Hide">
+import useSelect from '@/hooks/select';
+import { onBeforeUnmount, onMounted } from 'vue';
+
+const { isOne, canvasEditor } = useSelect();
+const isHide = ref(false);
+
+const doHide = (hide) => {
+  // 淇敼visible灞炴��
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  activeObject.set('visible', !hide);
+  canvasEditor.canvas.requestRenderAll();
+  isHide.value = hide;
+};
+
+const handleSelected = () => {
+  const activeObject = canvasEditor.canvas.getActiveObject();
+  isHide.value = !activeObject.visible;
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', handleSelected);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', handleSelected);
+});
+</script>
diff --git a/src/fabric-editor/components/history.vue b/src/fabric-editor/components/history.vue
new file mode 100644
index 0000000..a876c38
--- /dev/null
+++ b/src/fabric-editor/components/history.vue
@@ -0,0 +1,58 @@
+<template>
+  <div style="display: inline-block">
+    <!-- 鍚庨�� -->
+    <el-tooltip :content="'鎾ら攢' + `(${canUndo})`">
+      <el-button @click="undo" text size="small" :disabled="!canUndo">
+        <el-icon :size="20"><DArrowLeft /></el-icon>
+      </el-button>
+    </el-tooltip>
+
+    <!-- 閲嶅仛 -->
+    <el-tooltip :content="'閲嶅仛' + `(${canRedo})`">
+      <el-button @click="redo" text size="small" :disabled="!canRedo">
+        <el-icon :size="20"><DArrowRight /></el-icon>
+      </el-button>
+    </el-tooltip>
+  </div>
+</template>
+
+<script setup lang="ts">
+import useSelect from '@/fabric-editor/hooks/select';
+import { DArrowLeft, DArrowRight } from '@element-plus/icons-vue';
+const { canvasEditor } = useSelect() as { canvasEditor: any };
+const canUndo = ref(0);
+const canRedo = ref(0);
+// 鍚庨��
+const undo = () => {
+  canvasEditor.undo();
+};
+// 閲嶅仛
+const redo = () => {
+  canvasEditor.redo();
+};
+
+onMounted(() => {
+  canvasEditor.on('historyUpdate', (canUndoParam: number, canRedoParam: number) => {
+    canUndo.value = canUndoParam;
+    canRedo.value = canRedoParam;
+  });
+});
+</script>
+
+<style scoped lang="scss">
+span.active {
+  svg.icon {
+    fill: #2d8cf0;
+  }
+}
+
+.time {
+  color: #c1c1c1;
+}
+</style>
+
+<script lang="ts">
+export default {
+  name: 'ToolBar',
+};
+</script>
diff --git a/src/fabric-editor/components/imgStroke.vue b/src/fabric-editor/components/imgStroke.vue
new file mode 100644
index 0000000..60cdf24
--- /dev/null
+++ b/src/fabric-editor/components/imgStroke.vue
@@ -0,0 +1,170 @@
+<template>
+  <div class="box" v-if="isOne && isImage">
+    <!-- <Divider plain orientation="left">鍥惧儚鎻忚竟</Divider> -->
+    <Divider plain orientation="left">
+      <h4>鍥惧儚鎻忚竟</h4>
+    </Divider>
+    <div class="hd-wrap">
+      <div class="hd">
+        <span>鍚敤鍥惧儚鎻忚竟</span>
+        <Poptip trigger="hover" content="鍙敮鎸乸ng閫忔槑鍥惧儚">
+          <span><Icon type="ios-alert" color="#f34250" /></span>
+        </Poptip>
+      </div>
+
+      <iSwitch v-model="openImgStroke" size="large" class="switch" @on-change="onSwitchChange">
+        <template #open>
+          <span>寮�鍚�</span>
+        </template>
+        <template #close>
+          <span>鍏抽棴</span>
+        </template>
+      </iSwitch>
+    </div>
+
+    <template v-if="openImgStroke">
+      <div class="hd-wrap">
+        <div class="hd">
+          <span>鏄惁鍙樉绀烘弿杈�</span>
+        </div>
+
+        <iSwitch v-model="isOnlyStroke" size="large" class="switch" @on-change="updateStroke">
+          <template #open>
+            <span>鏄�</span>
+          </template>
+          <template #close>
+            <span>鍚�</span>
+          </template>
+        </iSwitch>
+      </div>
+      <div class="operation">
+        <div class="hd" style="flex-basis: 98px">
+          <span>鎻忚竟澶у皬</span>
+        </div>
+        <div style="width: 100%">
+          <Slider v-model="strokeWidth" :max="50" @on-change="onSliderChange"></Slider>
+        </div>
+      </div>
+
+      <div class="operation" style="justify-content: space-between">
+        <div class="hd">
+          <span>鎻忚竟棰滆壊</span>
+        </div>
+
+        <div>
+          <ColorPicker v-model="strokeColor" @on-change="onColorChange" placement="left" />
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script name="ImgStroke" lang="ts" setup>
+import useSelect from '@/hooks/select';
+import { Slider } from 'view-ui-plus';
+import { fabric } from 'fabric';
+import { Utils } from '@kuaitu/core';
+
+interface IExtendImage {
+  [x: string]: any;
+  originWidth?: number;
+  originHeight?: number;
+  originSrc?: string;
+}
+
+const { isOne, canvasEditor } = useSelect();
+const isImage = ref(false);
+const openImgStroke = ref(false);
+const strokeWidth = ref(5);
+const strokeColor = ref('#000');
+const isOnlyStroke = ref(false);
+const getActiveObject = (): (fabric.Image & IExtendImage) | undefined => {
+  const activeObject = canvasEditor.fabricCanvas?.getActiveObject();
+  if (!activeObject || !Utils.isImage(activeObject)) return;
+  return activeObject;
+};
+
+const setOrigin = () => {
+  const _activeObject = getActiveObject();
+  if (!_activeObject) return;
+  _activeObject.set('originWidth', _activeObject?.get('width'));
+  _activeObject.set('originHeight', _activeObject?.get('height'));
+  _activeObject.set('originSrc', _activeObject?.getSrc());
+};
+
+const updateStroke = () => {
+  const strokeType = unref(isOnlyStroke) ? 'destination-out' : 'source-over';
+  canvasEditor.imageStrokeDraw(unref(strokeColor), unref(strokeWidth), strokeType);
+};
+
+const closeImgStroke = () => {
+  strokeWidth.value = 0;
+  updateStroke();
+};
+
+const onSwitchChange = async (val: boolean) => {
+  if (val) {
+    unref(strokeWidth) === 0 && (strokeWidth.value = 5);
+    setOrigin();
+    updateStroke();
+  } else {
+    closeImgStroke();
+  }
+};
+
+const onSliderChange = (val: number) => {
+  strokeWidth.value = val;
+  updateStroke();
+};
+
+const onColorChange = (val: string) => {
+  strokeColor.value = val;
+  updateStroke();
+};
+
+const handleSelectOne = () => {
+  isImage.value = !!getActiveObject();
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', handleSelectOne);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', handleSelectOne);
+});
+</script>
+
+<style lang="less" scoped>
+// :deep(.ivu-divider-plain) {
+//   &.ivu-divider-with-text-left {
+//     margin: 10px 0;
+//     font-weight: bold;
+//     font-size: 16px;
+//     color: #000000;
+//   }
+// }
+.box {
+  margin-bottom: 20px;
+  .hd-wrap {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    .hd {
+      flex: 1;
+      & > span {
+        margin-right: 5px;
+      }
+    }
+  }
+  .operation {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .slide-wrap {
+      width: 100%;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/importFile.vue b/src/fabric-editor/components/importFile.vue
new file mode 100644
index 0000000..a8038fc
--- /dev/null
+++ b/src/fabric-editor/components/importFile.vue
@@ -0,0 +1,136 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-07-06 17:23:50
+ * @Description: 鎻掑叆SVG鍏冪礌
+-->
+
+<template>
+  <div style="display: inline-block">
+    <Dropdown transfer-class-name="fix" @on-click="insertTypeHand">
+      <a href="javascript:void(0)">
+        {{ $t('insertFile.insert') }}
+        <Icon type="ios-arrow-down"></Icon>
+      </a>
+      <template #list>
+        <DropdownMenu>
+          <!-- 鍥剧墖 -->
+          <DropdownItem name="insertImg">{{ $t('insertFile.insert_picture') }}</DropdownItem>
+          <!-- SVG -->
+          <DropdownItem name="insertSvg">{{ $t('insertFile.insert_SVG') }}</DropdownItem>
+          <!-- SVG 瀛楃涓� -->
+          <DropdownItem name="insertSvgStrModal">{{ $t('insertFile.insert_SVGStr') }}</DropdownItem>
+        </DropdownMenu>
+      </template>
+    </Dropdown>
+    <!-- 鎻掑叆瀛楃涓瞫vg鍏冪礌 -->
+    <Modal
+      v-model="state.showModal"
+      :title="$t('insertFile.modal_tittle')"
+      @on-ok="insertTypeHand('insertSvgStr')"
+      @on-cancel="showModal = false"
+    >
+      <Input
+        v-model="state.svgStr"
+        show-word-limit
+        type="textarea"
+        :placeholder="$t('insertFile.insert_SVGStr_placeholder')"
+      />
+    </Modal>
+  </div>
+</template>
+
+<script name="ImportFile" setup>
+import { Utils } from '@kuaitu/core';
+const { getImgStr, selectFiles } = Utils;
+
+import useSelect from '@/hooks/select';
+import { v4 as uuid } from 'uuid';
+
+const { fabric, canvasEditor } = useSelect();
+const state = reactive({
+  showModal: false,
+  svgStr: '',
+});
+const HANDLEMAP = {
+  // 鎻掑叆鍥剧墖
+  insertImg: function () {
+    selectFiles({ accept: 'image/*', multiple: true }).then((fileList) => {
+      Array.from(fileList).forEach((item) => {
+        getImgStr(item).then((file) => {
+          insertImgFile(file);
+        });
+      });
+    });
+  },
+  // 鎻掑叆Svg
+  insertSvg: function () {
+    selectFiles({ accept: '.svg', multiple: true }).then((fileList) => {
+      Array.from(fileList).forEach((item) => {
+        getImgStr(item).then((file) => {
+          insertSvgFile(file);
+        });
+      });
+    });
+  },
+  // 鎻掑叆SVG鍏冪礌
+  insertSvgStrModal: function () {
+    state.svgStr = '';
+    state.showModal = true;
+  },
+  // 鎻掑叆瀛楃涓插厓绱�
+  insertSvgStr: function () {
+    fabric.loadSVGFromString(state.svgStr, (objects, options) => {
+      const item = fabric.util.groupSVGElements(objects, {
+        ...options,
+        name: 'defaultSVG',
+      });
+      canvasEditor.addBaseType(item, {
+        scale: true,
+      });
+    });
+  },
+};
+
+const insertTypeHand = (type) => {
+  const cb = HANDLEMAP[type];
+  cb && typeof cb === 'function' && cb();
+};
+// 鎻掑叆鍥剧墖鏂囦欢
+function insertImgFile(file) {
+  if (!file) throw new Error('file is undefined');
+  const imgEl = document.createElement('img');
+  imgEl.src = file;
+  // 鎻掑叆椤甸潰
+  document.body.appendChild(imgEl);
+  imgEl.onload = async () => {
+    const imgItem = await canvasEditor.createImgByElement(imgEl);
+    canvasEditor.addBaseType(imgItem, {
+      scale: true,
+    });
+    imgEl.remove();
+  };
+}
+
+// 鎻掑叆鏂囦欢鍏冪礌
+function insertSvgFile(svgFile) {
+  if (!svgFile) throw new Error('file is undefined');
+  fabric.loadSVGFromURL(svgFile, (objects, options) => {
+    const item = fabric.util.groupSVGElements(objects, {
+      ...options,
+      name: 'defaultSVG',
+      id: uuid(),
+    });
+    canvasEditor.addBaseType(item, {
+      scale: true,
+    });
+  });
+}
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-select-dropdown) {
+  z-index: 999;
+}
+</style>
diff --git a/src/fabric-editor/components/importJSON.vue b/src/fabric-editor/components/importJSON.vue
new file mode 100644
index 0000000..c1460b5
--- /dev/null
+++ b/src/fabric-editor/components/importJSON.vue
@@ -0,0 +1,81 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-31 16:58:12
+ * @Description: 瀵煎叆JSON鏂囦欢
+-->
+
+<template>
+  <div style="display: inline-block">
+    <Dropdown @on-click="clickHandler">
+      <a href="javascript:void(0)">
+        {{ $t('importFiles.file') }}
+        <Icon type="ios-arrow-down"></Icon>
+      </a>
+      <template #list>
+        <DropdownMenu>
+          <DropdownItem name="createDesign">
+            {{ $t('importFiles.createDesign.title') }}
+          </DropdownItem>
+          <DropdownItem name="importFiles">{{ $t('importFiles.importFiles') }}</DropdownItem>
+          <DropdownItem name="psd">PSD</DropdownItem>
+        </DropdownMenu>
+      </template>
+    </Dropdown>
+
+    <!-- 鍒涘缓璁捐 -->
+    <modalSzie
+      :title="$t('importFiles.createDesign.title')"
+      ref="modalSizeRef"
+      @set="customSizeCreate"
+    ></modalSzie>
+  </div>
+</template>
+
+<script name="ImportJson" setup>
+import useSelect from '@/hooks/select';
+import useMaterial from '@/hooks/useMaterial';
+import { Message } from 'view-ui-plus';
+import modalSzie from '@/components/common/modalSzie';
+import { Spin } from 'view-ui-plus';
+
+const { canvasEditor } = useSelect();
+const { createTmpl, routerToId } = useMaterial();
+const modalSizeRef = ref(null);
+
+const clickHandler = (type) => {
+  const handleMap = {
+    // 瀵煎叆鏂囦欢
+    importFiles: canvasEditor.insert,
+    // 鍒涘缓鏂囦欢
+    createDesign,
+    // psd
+    psd: () => {
+      // Spin.show({
+      //   render: (h) => h('div', t('alert.loading_data')),
+      // });
+      canvasEditor.insertPSD().finally(Spin.hide);
+    },
+  };
+  handleMap[type]?.();
+};
+
+const createDesign = () => {
+  modalSizeRef.value.showSetSize();
+};
+
+const customSizeCreate = async (w, h) => {
+  const res = await createTmpl(w, h);
+  routerToId(res.data.data.id);
+  Message.success('鍒涘缓鎴愬姛');
+};
+</script>
+<style scoped lang="less">
+h3 {
+  margin-bottom: 10px;
+}
+.divider {
+  margin-top: 0;
+}
+</style>
diff --git a/src/fabric-editor/components/importTmpl.vue b/src/fabric-editor/components/importTmpl.vue
new file mode 100644
index 0000000..a08db81
--- /dev/null
+++ b/src/fabric-editor/components/importTmpl.vue
@@ -0,0 +1,180 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-08-16 17:47:21
+ * @Description: 瀵煎叆妯℃澘
+-->
+
+<template>
+  <div>
+    <!-- 鎼滅储缁勪欢 -->
+    <div class="search-box">
+      <Select
+        class="select"
+        v-model="typeValue"
+        @on-change="changeSelectType"
+        :disabled="pageLoading"
+      >
+        <Option v-for="item in typeList" :value="item.value" :key="item.value">
+          {{ item.label }}
+        </Option>
+      </Select>
+      <Input
+        class="input"
+        :placeholder="`鍦�${typeText}涓悳绱"
+        v-model="searchKeyWord"
+        search
+        :disabled="pageLoading"
+        @on-search="startGetList"
+      />
+    </div>
+    <!-- 鍒楄〃 -->
+    <div style="height: calc(100vh - 108px)" id="myTemplBox">
+      <Scroll
+        key="mysscroll"
+        v-if="showScroll"
+        :on-reach-bottom="nextPage"
+        :height="scrollHeight"
+        :distance-to-edge="[-1, -1]"
+      >
+        <!-- 鍒楄〃 -->
+        <div class="list-box">
+          <Tooltip :content="info.name" v-for="info in pageData" :key="info.src" placement="top">
+            <div class="tmpl-img-box">
+              <Image
+                lazy
+                :src="info.previewSrc"
+                fit="contain"
+                height="100%"
+                :alt="info.name"
+                @click="beforeClearTip(info)"
+              />
+            </div>
+          </Tooltip>
+        </div>
+        <Spin size="large" fix :show="pageLoading"></Spin>
+
+        <Divider plain v-if="isDownBottm">宸茬粡鍒板簳浜�</Divider>
+      </Scroll>
+    </div>
+  </div>
+</template>
+
+<script setup name="ImportTmpl">
+import useSelect from '@/hooks/select';
+import usePageList from '@/hooks/pageList';
+import { Spin, Modal } from 'view-ui-plus';
+import { debounce } from 'lodash-es';
+
+import { useI18n } from 'vue-i18n';
+import { useRouter, useRoute } from 'vue-router';
+const router = useRouter();
+const route = useRoute();
+const { t } = useI18n();
+const { canvasEditor } = useSelect();
+
+const {
+  startPage,
+  typeValue,
+  typeText,
+  typeList,
+  pageLoading,
+  pageData,
+  searchKeyWord,
+  isDownBottm,
+  startGetList,
+  nextPage,
+  showScroll,
+  scrollHeight,
+  getInfo,
+} = usePageList({
+  typeUrl: 'templ-types',
+  listUrl: 'templs',
+  searchTypeKey: 'templ_type',
+  searchWordKey: 'name',
+  pageSize: 10,
+  scrollElement: '#myTemplBox',
+  fields: ['name'],
+});
+
+typeValue.value = 4;
+
+// 鏇挎崲鎻愮ず
+const beforeClearTip = (info) => {
+  Modal.confirm({
+    title: t('tip'),
+    content: `<p>${t('replaceTip')}</p>`,
+    okText: t('ok'),
+    cancelText: t('cancel'),
+    onOk: () => getTempData(info),
+  });
+};
+
+onMounted(() => {
+  startPage();
+  getTemplInfo();
+});
+
+// 鑾峰彇妯℃澘鏁版嵁
+const getTempData = async (info) => {
+  Spin.show({
+    render: (h) => h('div', t('alert.loading_data')),
+  });
+  const infoRes = await getInfo(info.id);
+  if (route.query.admin) {
+    router.replace('/?tempId=' + info.id + '&admin=true');
+  } else {
+    router.replace('/?tempId=' + info.id);
+  }
+  canvasEditor.loadJSON(JSON.stringify(infoRes.data.data.attributes.json), Spin.hide);
+};
+
+const getTemplInfo = async () => {
+  if (route.query.tempId) {
+    try {
+      const infoRes = await getInfo(route.query.tempId);
+      canvasEditor.loadJSON(JSON.stringify(infoRes.data.data.attributes.json), Spin.hide);
+    } catch (error) {
+      console.log(error);
+    }
+  }
+};
+
+const changeSelectType = debounce(() => {
+  startGetList();
+}, 100);
+</script>
+
+<style scoped lang="less">
+.search-box {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  display: flex;
+  .input {
+    margin-left: 10px;
+  }
+  .select {
+    width: 100px;
+  }
+}
+
+.list-box {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+
+.tmpl-img-box {
+  width: 140px;
+  cursor: pointer;
+  border-radius: 5px;
+  overflow: hidden;
+  &:hover {
+    :deep(.ivu-image-img) {
+      opacity: 0.8;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/inputNumber/index.ts b/src/fabric-editor/components/inputNumber/index.ts
new file mode 100644
index 0000000..0dd7661
--- /dev/null
+++ b/src/fabric-editor/components/inputNumber/index.ts
@@ -0,0 +1,3 @@
+import InputNumber from './inputNumber.vue';
+
+export default InputNumber;
diff --git a/src/fabric-editor/components/inputNumber/inputNumber.vue b/src/fabric-editor/components/inputNumber/inputNumber.vue
new file mode 100644
index 0000000..d4eb293
--- /dev/null
+++ b/src/fabric-editor/components/inputNumber/inputNumber.vue
@@ -0,0 +1,555 @@
+<script setup lang="ts">
+// https://github.com/view-design/ViewUIPlus/blob/master/src/components/input-number/input-number.vue
+// https://github.com/arco-design/arco-design-vue/blob/main/packages/web-vue/components/input-number/input-number.tsx
+import { usePointerSwipe, type MaybeRefOrGetter } from '@vueuse/core';
+import { isNumber, isUndefined } from 'lodash-es';
+import NP from 'number-precision';
+
+NP.enableBoundaryChecking(false);
+
+type StepMethods = 'minus' | 'plus';
+
+const props = withDefaults(
+  defineProps<{
+    autofocus?: boolean;
+    /**
+     * @zh 浠� `formatter` 杞崲涓烘暟瀛楋紝鍜� `formatter` 鎼厤浣跨敤
+     * @en Convert from `formatter` to number, and use with `formatter`
+     */
+    parser?: (val: string) => string;
+    /**
+     * @zh 瀹氫箟杈撳叆妗嗗睍绀哄��
+     * @en Define the display value of the input
+     */
+    formatter?: (val: string) => string;
+    /**
+     * @zh 鏈�灏忓��
+     * @en Min
+     */
+    min?: number;
+    /**
+     * @zh 鏈�澶у��
+     * @en Max
+     */
+    max?: number;
+    /**
+     * @zh 鏁板瓧绮惧害
+     * @en Precision
+     */
+    precision?: number;
+    /**
+     * @zh 鏁板瓧鍙樺寲姝ラ暱
+     * @en Number change step
+     */
+    step?: number;
+    /**
+     * @zh 缁戝畾鍊�
+     * @en Value
+     */
+    modelValue?: number;
+    /**
+     * @zh 榛樿鍊硷紙闈炲彈鎺фā寮忥級
+     * @en Default value (uncontrolled mode)
+     */
+    defaultValue?: number;
+    /**
+     * @zh 瑙﹀彂 `v-model` 鐨勪簨浠�
+     * @en Trigger event for `v-model`
+     */
+    modelEvent?: 'change' | 'input';
+    /**
+     * @zh 妯″紡锛坒alse锛歟mbed鎸夐挳鍐呭祵妯″紡锛宼rue锛歜utton宸﹀彸鎸夐挳妯″紡锛�
+     */
+    controlsOutside?: boolean;
+    /**
+     * @zh 鏄惁闅愯棌鎸夐挳锛堜粎鍦╜embed`妯″紡鍙敤锛�
+     * @en Whether to hide the button (only available in `embed` mode)
+     */
+    hideButton?: boolean;
+    downDisabled?: boolean;
+    upDisabled?: boolean;
+    /**
+     * @zh 杈撳叆妗嗗ぇ灏�
+     * @en Input size
+     * @values 'small','large','default'
+     * @defaultValue 'default'
+     */
+    size?: 'small' | 'large' | 'default';
+    /**
+     * @zh 鏄惁绂佺敤
+     * @en Whether to disable
+     */
+    disabled?: boolean;
+    /**
+     * @zh 鍙
+     * @en Readonly
+     * @version 3.33.1
+     */
+    readonly?: boolean;
+    /**
+     * @zh 杈撳叆妗嗘彁绀烘枃瀛�
+     * @en Input prompt text
+     */
+    placeholder?: string;
+    append?: string;
+    prepend?: string;
+  }>(),
+  {
+    min: -Infinity,
+    max: Infinity,
+    step: 1,
+    modelEvent: 'change',
+    size: 'default',
+    precision: 2,
+  }
+);
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: number | undefined): void;
+  (e: 'on-change', value: number): void;
+  (e: 'on-focus', value: FocusEvent): void;
+  (e: 'on-blur', value: FocusEvent): void;
+  (e: 'on-input', value: number | undefined, inputValue: string, ev: Event): void;
+  (e: 'on-change', value: number | undefined, ev: Event): void;
+}>();
+
+const prefixCls = 'ivu-input-number';
+const iconPrefixCls = 'ivu-icon';
+const inputWrapClasses = ref(`${prefixCls}-input-wrap`);
+const inputClasses = ref(`${prefixCls}-input`);
+const handlerClasses = ref(`${prefixCls}-handler-wrap`);
+const innerUpClasses = ref(
+  `${prefixCls}-handler-up-inner ${iconPrefixCls} ${iconPrefixCls}-ios-arrow-up`
+);
+const innerDownClasses = ref(
+  `${prefixCls}-handler-down-inner ${iconPrefixCls} ${iconPrefixCls}-ios-arrow-down`
+);
+const upClasses = ref([
+  `${prefixCls}-handler`,
+  `${prefixCls}-handler-up`,
+  {
+    [`${prefixCls}-handler-up-disabled`]: props.upDisabled,
+  },
+]);
+const downClasses = ref([
+  `${prefixCls}-handler`,
+  `${prefixCls}-handler-down`,
+  {
+    [`${prefixCls}-handler-down-disabled`]: props.downDisabled,
+  },
+]);
+
+const focused = ref(false);
+
+const wrapClasses = computed(() => {
+  return [
+    `${prefixCls}`,
+    {
+      [`${prefixCls}-${props.size}`]: !!props.size,
+      [`${prefixCls}-disabled`]: props.disabled,
+      [`${prefixCls}-focused`]: focused.value,
+      [`${prefixCls}-controls-outside`]: props.controlsOutside,
+    },
+  ];
+});
+
+const inputRef = ref<HTMLInputElement>();
+
+const mergedPrecision = computed(() => {
+  if (isNumber(props.precision)) {
+    const decimal = `${props.step}`.split('.')[1];
+    const stepPrecision = (decimal && decimal.length) || 0;
+    return Math.max(stepPrecision, props.precision);
+  }
+  return undefined;
+});
+
+const getStringValue = (number: number | undefined) => {
+  if (!isNumber(number)) {
+    return '';
+  }
+
+  const numString = mergedPrecision.value
+    ? number.toFixed(mergedPrecision.value).replace(/\.?0+$/, '')
+    : String(number);
+  return props.formatter?.(numString) ?? numString;
+};
+
+const _value = ref(getStringValue(props.modelValue ?? props.defaultValue));
+
+const handleFocus = (event: FocusEvent) => {
+  focused.value = true;
+  emit('on-focus', event);
+};
+
+const handleBlur = (event: FocusEvent) => {
+  focused.value = false;
+  emit('on-blur', event);
+};
+
+const minus = (e: Event) => {
+  handleStepButton(e, 'minus', true);
+};
+const plus = (e: Event) => {
+  handleStepButton(e, 'plus', true);
+};
+
+const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === 'ArrowUp') {
+    e.preventDefault();
+    !props.readonly && nextStep('plus', e);
+  } else if (e.key === 'ArrowDown') {
+    e.preventDefault();
+    !props.readonly && nextStep('minus', e);
+  }
+};
+
+const nextStep = (method: StepMethods, event: Event) => {
+  if (
+    (method === 'plus' && (isMax.value || props.upDisabled)) ||
+    (method === 'minus' && (isMin.value || props.downDisabled))
+  ) {
+    return;
+  }
+  let nextValue: number | undefined;
+  if (isNumber(valueNumber.value)) {
+    nextValue = getLegalValue(NP[method](valueNumber.value, props.step));
+  } else {
+    nextValue = props.min === -Infinity ? 0 : props.min;
+  }
+  _value.value = getStringValue(nextValue);
+  updateNumberStatus(nextValue);
+  emit('update:modelValue', nextValue);
+  emit('on-change', nextValue, event);
+};
+
+// 姝ラ暱閲嶅瀹氭椂鍣�
+let repeatTimer = 0;
+const SPEED = 150;
+const clearRepeatTimer = () => {
+  if (repeatTimer) {
+    window.clearTimeout(repeatTimer);
+    repeatTimer = 0;
+  }
+};
+const clearRepeatTimerProps = {
+  onMouseup: clearRepeatTimer,
+  onMouseleave: clearRepeatTimer,
+};
+
+const handleStepButton = (event: Event, method: StepMethods, needRepeat = false) => {
+  event.preventDefault();
+  inputRef.value?.focus();
+  nextStep(method, event);
+  // 闀挎寜鏃舵寔缁Е鍙�
+  if (needRepeat) {
+    repeatTimer = window.setTimeout(
+      () => (event.target as HTMLElement).dispatchEvent(event),
+      SPEED
+    );
+  }
+};
+
+const valueNumber = computed(() => {
+  if (!_value.value) {
+    return undefined;
+  }
+  const number = Number(props.parser?.(_value.value) ?? _value.value);
+  return Number.isNaN(number) ? undefined : number;
+});
+
+const isMin = ref(isNumber(valueNumber.value) && valueNumber.value <= props.min);
+const isMax = ref(isNumber(valueNumber.value) && valueNumber.value >= props.max);
+
+const updateNumberStatus = (number: number | undefined) => {
+  let _isMin = false;
+  let _isMax = false;
+  if (isNumber(number)) {
+    if (number <= props.min) {
+      _isMin = true;
+    }
+    if (number >= props.max) {
+      _isMax = true;
+    }
+  }
+  if (isMax.value !== _isMax) {
+    isMax.value = _isMax;
+  }
+  if (isMin.value !== _isMin) {
+    isMin.value = _isMin;
+  }
+};
+
+const handleInput = (e: Event) => {
+  let { value } = e.target as HTMLInputElement;
+  value = value.trim().replace(/銆�/g, '.');
+  value = props.parser?.(value) ?? value;
+  if (isNumber(Number(value)) || /^(\.|-)$/.test(value)) {
+    _value.value = props.formatter?.(value) ?? value;
+    updateNumberStatus(valueNumber.value);
+    if (props.modelEvent === 'input') {
+      emit('update:modelValue', valueNumber.value);
+    }
+    emit('on-input', valueNumber.value, _value.value, e);
+  }
+};
+
+const getLegalValue = (value: number | undefined): number | undefined => {
+  if (isUndefined(value)) {
+    return undefined;
+  }
+  if (isNumber(props.min) && value < props.min) {
+    value = props.min;
+  }
+  if (isNumber(props.max) && value > props.max) {
+    value = props.max;
+  }
+  return isNumber(mergedPrecision.value) ? NP.round(value, mergedPrecision.value) : value;
+};
+
+const handleChange = (e: Event) => {
+  const finalValue = getLegalValue(valueNumber.value);
+  const stringValue = getStringValue(finalValue);
+  if (finalValue !== valueNumber.value || _value.value !== stringValue) {
+    _value.value = stringValue;
+    updateNumberStatus(finalValue);
+  }
+  nextTick(() => {
+    if (isNumber(props.modelValue) && props.modelValue !== finalValue) {
+      _value.value = getStringValue(props.modelValue);
+      updateNumberStatus(props.modelValue);
+    }
+  });
+  emit('update:modelValue', finalValue);
+  emit('on-change', finalValue, e);
+};
+
+const handleExceedRange = () => {
+  const finalValue = getLegalValue(valueNumber.value);
+  const stringValue = getStringValue(finalValue);
+  if (finalValue !== valueNumber.value || _value.value !== stringValue) {
+    _value.value = stringValue;
+  }
+  emit('update:modelValue', finalValue);
+};
+
+// 婊戝姩鐩稿叧
+const appendLabelRef = ref<HTMLElement>();
+const prependLabelRef = ref<HTMLElement>();
+const useSwipe = (target: MaybeRefOrGetter<HTMLElement | null | undefined>) => {
+  const startValue = ref<number>();
+  const { posStart, posEnd } = usePointerSwipe(target, {
+    threshold: 0,
+    onSwipeStart: () => {
+      startValue.value = valueNumber.value;
+    },
+    onSwipe: (e: PointerEvent) => {
+      if (!isNumber(startValue.value)) return;
+      const newValue = startValue.value + NP.round(posEnd.x - posStart.x, 0) * props.step;
+      _value.value = getStringValue(newValue);
+      props.modelEvent === 'input' ? handleInput(e) : handleChange(e);
+    },
+  });
+};
+
+// mounted
+onMounted(() => {
+  appendLabelRef.value && useSwipe(appendLabelRef);
+  prependLabelRef.value && useSwipe(prependLabelRef);
+});
+
+// watch
+watch(
+  () => props.modelValue,
+  (value: number | undefined) => {
+    if (value !== valueNumber.value) {
+      _value.value = getStringValue(value);
+      updateNumberStatus(value);
+    }
+  }
+);
+watch(
+  () => props.min,
+  (newVal) => {
+    const _isMin = isNumber(valueNumber.value) && valueNumber.value <= newVal;
+    if (isMin.value !== _isMin) {
+      isMin.value = _isMin;
+    }
+
+    const isExceedMinValue = isNumber(valueNumber.value) && valueNumber.value < newVal;
+    if (isExceedMinValue) {
+      handleExceedRange();
+    }
+  }
+);
+watch(
+  () => props.max,
+  (newVal) => {
+    const _isMax = isNumber(valueNumber.value) && valueNumber.value >= newVal;
+    if (isMax.value !== _isMax) {
+      isMax.value = _isMax;
+    }
+
+    const isExceedMaxValue = isNumber(valueNumber.value) && valueNumber.value > newVal;
+    if (isExceedMaxValue) {
+      handleExceedRange();
+    }
+  }
+);
+
+// 瀵煎嚭鍑芥暟
+defineExpose({
+  minus,
+  plus,
+  input: inputRef.value,
+  /**
+   * @zh 浣胯緭鍏ユ鑾峰彇鐒︾偣
+   * @en Make the input box focus
+   * @public
+   */
+  focus: () => {
+    inputRef.value?.focus();
+  },
+  /**
+   * @zh 浣胯緭鍏ユ澶卞幓鐒︾偣
+   * @en Make the input box lose focus
+   * @public
+   */
+  blur: () => {
+    inputRef.value?.blur();
+  },
+});
+</script>
+
+<template>
+  <div :class="wrapClasses">
+    <template v-if="controlsOutside">
+      <div
+        class="ivu-input-number-controls-outside-btn ivu-input-number-controls-outside-down"
+        :class="{ 'ivu-input-number-controls-outside-btn-disabled': downDisabled }"
+        @mousedown="minus"
+        v-bind="clearRepeatTimerProps"
+      >
+        <i class="ivu-icon ivu-icon-ios-remove"></i>
+      </div>
+      <div
+        class="ivu-input-number-controls-outside-btn ivu-input-number-controls-outside-up"
+        :class="{ 'ivu-input-number-controls-outside-btn-disabled': upDisabled }"
+        @mousedown="plus"
+        v-bind="clearRepeatTimerProps"
+      >
+        <i class="ivu-icon ivu-icon-ios-add"></i>
+      </div>
+    </template>
+    <div :class="handlerClasses" v-else-if="!hideButton">
+      <a :class="upClasses" @mousedown="plus" v-bind="clearRepeatTimerProps">
+        <span :class="innerUpClasses"></span>
+      </a>
+      <a :class="downClasses" @mousedown="minus" v-bind="clearRepeatTimerProps">
+        <span :class="innerDownClasses"></span>
+      </a>
+    </div>
+    <div :class="inputWrapClasses">
+      <template v-if="$slots.prefix">
+        <slot name="prefix"></slot>
+      </template>
+      <label ref="appendLabelRef" :class="`${inputWrapClasses}__label`" v-else-if="append">
+        {{ append }}
+      </label>
+      <input
+        ref="inputRef"
+        type="text"
+        autocomplete="off"
+        spellcheck="false"
+        role="spinbutton"
+        :aria-valuemax="max"
+        :aria-valuemin="min"
+        :aria-valuenow="_value"
+        :value="_value"
+        :class="inputClasses"
+        :disabled="disabled"
+        :autofocus="autofocus"
+        :readonly="readonly"
+        :placeholder="placeholder"
+        @focus="handleFocus"
+        @blur="handleBlur"
+        @input="handleInput"
+        @change="handleChange"
+        @keydown="handleKeyDown"
+      />
+      <template v-if="$slots.suffix">
+        <slot name="suffix"></slot>
+      </template>
+      <label ref="prependLabelRef" :class="`${inputWrapClasses}__label`" v-else-if="prepend">
+        {{ prepend }}
+      </label>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+@import 'view-ui-plus/src/styles/custom.less';
+
+@input-number-prefix-cls: ~'@{css-prefix}input-number';
+
+.@{input-number-prefix-cls} {
+  border: none;
+  background: @input-group-bg;
+
+  &-input {
+    background: none;
+  }
+
+  &-handler {
+    height: (@input-height-base / 2);
+
+    &-wrap {
+      background: @input-group-bg;
+      border-left-color: transparent;
+    }
+
+    &-down {
+      border-top: none;
+    }
+
+    &-up-inner,
+    &-down-inner {
+      line-height: (@input-height-base / 2);
+    }
+  }
+
+  &-input-wrap {
+    display: flex;
+    align-items: center;
+
+    &__label {
+      flex-shrink: 0;
+      padding: 0 10px;
+      user-select: none;
+      cursor: ew-resize;
+    }
+  }
+
+  &-small {
+    .@{input-number-prefix-cls}-handler {
+      height: (@input-height-small / 2);
+
+      &-up-inner,
+      &-down-inner {
+        line-height: (@input-height-small / 2);
+      }
+    }
+  }
+
+  &-large {
+    .@{input-number-prefix-cls}-handler {
+      height: (@input-height-large / 2);
+
+      &-up-inner,
+      &-down-inner {
+        line-height: (@input-height-large / 2);
+      }
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/lang.vue b/src/fabric-editor/components/lang.vue
new file mode 100644
index 0000000..912a97d
--- /dev/null
+++ b/src/fabric-editor/components/lang.vue
@@ -0,0 +1,52 @@
+<!--
+ * @Descripttion:
+ * @version:
+ * @Author: June
+ * @Date: 2023-05-20 09:18:28
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2023-07-29 22:24:03
+-->
+<template>
+  <Dropdown placement="bottom-end" @on-click="setLang">
+    <Button type="text">
+      {{ lang }}
+      <Icon type="ios-arrow-down"></Icon>
+    </Button>
+    <template #list>
+      <DropdownMenu>
+        <DropdownItem v-for="lang in langList" :key="lang.langType" :name="lang.langType">
+          {{ lang.langName }}
+        </DropdownItem>
+      </DropdownMenu>
+    </template>
+  </Dropdown>
+</template>
+
+<script setup name="saveBar">
+import { setLocal } from '@/utils/local';
+import { LANG } from '@/config/constants/app';
+
+import { useI18n } from 'vue-i18n';
+const { locale } = useI18n();
+
+const LANGMAP = {
+  zh: '涓枃',
+  en: 'En',
+};
+
+let langList = reactive(
+  Object.keys(LANGMAP).map((key) => ({ langType: key, langName: LANGMAP[key] }))
+);
+
+const lang = computed(() => {
+  return LANGMAP[locale.value];
+});
+
+// 璁剧疆璇█
+const setLang = (type) => {
+  locale.value = type;
+  setLocal(LANG, type);
+};
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/layer.vue b/src/fabric-editor/components/layer.vue
new file mode 100644
index 0000000..4a19c71
--- /dev/null
+++ b/src/fabric-editor/components/layer.vue
@@ -0,0 +1,258 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:20:43
+ * @Description: 鍥惧眰闈㈡澘
+-->
+
+<template>
+  <div class="box">
+    <template v-if="list.length">
+      <Divider plain orientation="left">{{ $t('layers') }}</Divider>
+      <div class="layer-box">
+        <div
+          v-for="item in list"
+          @click="select(item.id)"
+          :key="item.id"
+          :class="isSelect(item) && 'active'"
+        >
+          <Row class="ellipsis">
+            <Col span="20">
+              <Tooltip :content="item.name || item.text || item.type" placement="left">
+                <span :class="isSelect(item) && 'active'" v-html="iconType(item.type)"></span>
+                | {{ textType(item.type, item) }}
+              </Tooltip>
+            </Col>
+            <Col span="4">
+              <Button
+                long
+                :icon="item.isLock ? 'md-lock' : 'md-unlock'"
+                type="text"
+                @click="doLock(item)"
+              ></Button>
+            </Col>
+          </Row>
+        </div>
+      </div>
+      <!-- 灞傜骇璋冩暣鎸夐挳 -->
+      <div class="btn-box">
+        <ButtonGroup v-show="isOne" size="small">
+          <Button @click="up"><span v-html="btnIconType('up')"></span></Button>
+          <Button @click="down"><span v-html="btnIconType('down')"></span></Button>
+          <Button @click="upTop"><span v-html="btnIconType('upTop')"></span></Button>
+          <Button @click="downTop"><span v-html="btnIconType('downTop')"></span></Button>
+        </ButtonGroup>
+      </div>
+    </template>
+    <template v-else>
+      <p class="empty-text">鏆傛棤鍥惧眰</p>
+    </template>
+  </div>
+</template>
+
+<script setup name="Layer">
+import { uniqBy } from 'lodash-es';
+import useSelect from '@/hooks/select';
+import groupIcon from '@/assets/icon/layer/group.svg?raw';
+import textbox from '@/assets/icon/layer/textbox.svg?raw';
+import iText from '@/assets/icon/layer/iText.svg?raw';
+import imageIcon from '@/assets/icon/layer/image.svg?raw';
+import rectIcon from '@/assets/icon/layer/rect.svg?raw';
+import circleIcon from '@/assets/icon/layer/circle.svg?raw';
+import triangleIcon from '@/assets/icon/layer/triangle.svg?raw';
+import polygonIcon from '@/assets/icon/layer/polygon.svg?raw';
+
+import upIcon from '@/assets/icon/layer/up.svg?raw';
+import downIcon from '@/assets/icon/layer/down.svg?raw';
+import upTopIcon from '@/assets/icon/layer/upTop.svg?raw';
+import downTopIcon from '@/assets/icon/layer/downTop.svg?raw';
+
+const { canvasEditor, isOne, fabric, mixinState } = useSelect();
+
+const list = ref([]);
+
+// 鏄惁閫変腑鍏冪礌
+const isSelect = (item) => {
+  return item.id === mixinState.mSelectId || mixinState.mSelectIds.includes(item.id);
+};
+
+// 鍥惧眰绫诲瀷鍥炬爣
+const iconType = (type) => {
+  const iconType = {
+    group: groupIcon,
+    textbox: textbox,
+    'i-text': iText,
+    image: imageIcon,
+    rect: rectIcon,
+    circle: circleIcon,
+    triangle: triangleIcon,
+    polygon: polygonIcon,
+  };
+  const defaultIcon = '';
+  return iconType[type] || defaultIcon;
+};
+const textType = (type, item) => {
+  if (type.includes('text')) {
+    return item.name || item.text;
+  }
+  const typeText = {
+    group: '缁勫悎',
+    image: '鍥剧墖',
+    rect: '鐭╁舰',
+    circle: '鍦嗗舰',
+    triangle: '涓夎褰�',
+    polygon: '澶氳竟褰�',
+    path: '璺緞',
+  };
+  return typeText[type] || '榛樿鍏冪礌';
+};
+// 閫変腑鍏冪礌
+const select = (id) => {
+  const info = canvasEditor.canvas.getObjects().find((item) => item.id === id);
+  canvasEditor.canvas.discardActiveObject();
+  canvasEditor.canvas.setActiveObject(info);
+  canvasEditor.canvas.requestRenderAll();
+};
+
+// 鎸夐挳绫诲瀷
+const btnIconType = (type) => {
+  const iconType = {
+    up: upIcon,
+    down: downIcon,
+    upTop: upTopIcon,
+    downTop: downTopIcon,
+  };
+  return iconType[type];
+};
+const up = () => {
+  canvasEditor.up();
+};
+const upTop = () => {
+  canvasEditor.toFront();
+};
+const down = () => {
+  canvasEditor.down();
+};
+const downTop = () => {
+  canvasEditor.toBack();
+};
+
+const getList = () => {
+  // 涓嶆敼鍘熸暟缁� 鍙嶈浆
+  list.value = [
+    ...canvasEditor.canvas.getObjects().filter((item) => {
+      // return item;
+      // 杩囨护鎺夎緟鍔╃嚎
+      return !(item instanceof fabric.GuideLine || item.id === 'workspace');
+    }),
+  ]
+    .reverse()
+    .map((item) => {
+      const { type, id, name, text, selectable } = item;
+      return {
+        type,
+        id,
+        name,
+        text,
+        isLock: !selectable,
+      };
+    });
+  list.value = uniqBy(unref(list), 'id');
+};
+
+const doLock = (item) => {
+  select(item.id);
+  item.isLock ? canvasEditor.unLock() : canvasEditor.lock();
+  canvasEditor.canvas.discardActiveObject();
+};
+
+onMounted(() => {
+  getList();
+  canvasEditor.canvas.on('after:render', getList);
+});
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="less">
+:deep(.ivu-tooltip-inner) {
+  white-space: normal;
+}
+
+:deep(.ivu-tooltip) {
+  display: block;
+}
+
+// :deep(.ivu-tooltip-rel) {
+//   display: block;
+// }
+.box {
+  width: 100%;
+}
+.layer-box {
+  height: calc(100vh - 170px);
+  overflow-y: auto;
+  margin-bottom: 5px;
+  .ellipsis {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    cursor: pointer;
+  }
+  & > div {
+    padding: 0px 5px;
+    margin: 3px 0;
+    background: #f7f7f7;
+    color: #c8c8c8;
+    border-radius: 3px;
+    font-size: 14px;
+    line-height: 28px;
+    &.active {
+      color: #2d8cf0;
+      background: #f0faff;
+      font-weight: bold;
+    }
+  }
+}
+.btn-box {
+  width: 100%;
+  margin-bottom: 20px;
+  background: #f3f3f3;
+  .ivu-btn-group {
+    display: flex;
+  }
+  .ivu-btn-group > .ivu-btn {
+    flex: 1;
+  }
+}
+svg {
+  vertical-align: text-top;
+}
+:deep(.ivu-divider-plain) {
+  &.ivu-divider-with-text-left {
+    margin: 10px 0;
+    font-size: 16px;
+    font-weight: bold;
+    color: #000000;
+  }
+}
+.empty-text {
+  width: 100%;
+  text-align: center;
+  padding-top: 10px;
+  color: #999;
+}
+</style>
+
+<style lang="less">
+span {
+  svg {
+    vertical-align: middle;
+  }
+  &.active {
+    svg.icon {
+      fill: #2d8cf0;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/lock.vue b/src/fabric-editor/components/lock.vue
new file mode 100644
index 0000000..0f5948a
--- /dev/null
+++ b/src/fabric-editor/components/lock.vue
@@ -0,0 +1,75 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:20:59
+ * @Description: 閿佸畾鍏冪礌
+-->
+
+<template>
+  <el-tooltip :content="isLock ? '瑙i攣' : '閿佸畾'" v-if="isOne && isMatchType">
+    <el-button v-if="isLock" @click="doLock(false)" icon="Lock" text></el-button>
+    <el-button v-else @click="doLock(true)" icon="Unlock" text></el-button>
+  </el-tooltip>
+</template>
+
+<script setup name="Lock">
+import useSelect from '@/fabric-editor/hooks/select';
+import { onBeforeUnmount, onMounted } from 'vue';
+import { TemplateParamObjectName } from '@/fabric-editor/customObject';
+
+// 鍙慨鏀圭殑鍏冪礌
+const baseType = [
+  'text',
+  'i-text',
+  'textbox',
+  'rect',
+  'circle',
+  'triangle',
+  'polygon',
+  // 'image',
+  'group',
+  'line',
+  'arrow',
+  'thinTailArrow',
+  TemplateParamObjectName,
+];
+const { isMatchType, canvasEditor, isOne } = useSelect(baseType);
+
+const isLock = ref(false);
+const doLock = (isLock) => {
+  isLock ? canvasEditor.lock() : canvasEditor.unLock();
+};
+
+const handleSelected = (items) => {
+  isLock.value = items[0].lockMovementX;
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', handleSelected);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', handleSelected);
+});
+</script>
+
+<style scoped lang="scss">
+h3 {
+  margin: 40px 0 0;
+}
+
+ul {
+  list-style-type: none;
+  padding: 0;
+}
+
+li {
+  display: inline-block;
+  margin: 0 10px;
+}
+
+a {
+  color: #42b983;
+}
+</style>
diff --git a/src/fabric-editor/components/login.vue b/src/fabric-editor/components/login.vue
new file mode 100644
index 0000000..acb0d37
--- /dev/null
+++ b/src/fabric-editor/components/login.vue
@@ -0,0 +1,244 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-04-24 12:51:24
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-04-24 15:10:46
+ * @Description: 鐧诲綍
+-->
+
+<template>
+  <div class="box">
+    <!-- 鐧诲綍鍚� -->
+    <Dropdown v-if="userName" @on-click="logoutHandle">
+      <a href="javascript:void(0)">
+        <Icon type="ios-person" size="20" />
+        {{ userName }}
+        <Icon type="ios-arrow-down"></Icon>
+      </a>
+      <template #list>
+        <DropdownMenu>
+          <DropdownItem>閫�鍑虹櫥褰�</DropdownItem>
+        </DropdownMenu>
+      </template>
+    </Dropdown>
+    <!-- 鐧诲綍鍓� -->
+    <Button v-else shape="circle" icon="ios-person" @click="modal = true"></Button>
+
+    <!-- 鐧诲綍娉ㄥ唽寮规 -->
+    <Modal v-model="modal" footer-hide>
+      <h3>{{ $t('login.title') }}</h3>
+      <Tabs :animated="false" @on-click="switchTab">
+        <!-- 鐧诲綍 -->
+        <TabPane :label="$t('login.login')">
+          <Form ref="loginForm" :model="formInline" :rules="ruleInline" class="form-box">
+            <FormItem prop="identifier">
+              <Input
+                type="text"
+                v-model="formInline.identifier"
+                :placeholder="$t('login.identifier')"
+              >
+                <template #prepend>
+                  <Icon type="ios-person-outline"></Icon>
+                </template>
+              </Input>
+            </FormItem>
+            <FormItem prop="password">
+              <Input
+                type="password"
+                v-model="formInline.password"
+                :placeholder="$t('login.password')"
+              >
+                <template #prepend>
+                  <Icon type="ios-lock-outline"></Icon>
+                </template>
+              </Input>
+            </FormItem>
+            <FormItem>
+              <Button type="primary" long @click="loginHandle">
+                {{ $t('login.login') }}
+              </Button>
+            </FormItem>
+          </Form>
+        </TabPane>
+
+        <!-- 娉ㄥ唽 -->
+        <TabPane :label="$t('login.register')">
+          <Form
+            ref="registerForm"
+            :model="registerFormInline"
+            :rules="registerRuleInline"
+            class="form-box"
+          >
+            <FormItem prop="username">
+              <Input
+                type="text"
+                v-model="registerFormInline.username"
+                :placeholder="$t('login.username')"
+              >
+                <template #prepend>
+                  <Icon type="ios-person-outline"></Icon>
+                </template>
+              </Input>
+            </FormItem>
+            <FormItem prop="email">
+              <Input
+                type="text"
+                v-model="registerFormInline.email"
+                :placeholder="$t('login.email')"
+              >
+                <template #prepend>
+                  <Icon type="ios-mail-outline" />
+                </template>
+              </Input>
+            </FormItem>
+
+            <FormItem prop="password">
+              <Input
+                type="password"
+                v-model="registerFormInline.password"
+                :placeholder="$t('login.password')"
+              >
+                <template #prepend>
+                  <Icon type="ios-lock-outline"></Icon>
+                </template>
+              </Input>
+            </FormItem>
+            <FormItem>
+              <Button type="primary" long @click="registerHandle">
+                {{ $t('login.register') }}
+              </Button>
+            </FormItem>
+          </Form>
+        </TabPane>
+      </Tabs>
+    </Modal>
+  </div>
+</template>
+
+<script setup name="Login">
+import { useI18n } from 'vue-i18n';
+import { getUserInfo, setToken, login, register, logout } from '@/api/user';
+import { Message, Modal } from 'view-ui-plus';
+const modal = ref(false);
+const { t } = useI18n();
+const userName = ref('');
+// 鑾峰彇鐢ㄦ埛璇︽儏
+getUserInfo()
+  .then((res) => {
+    userName.value = res.data.username;
+  })
+  .catch(() => {
+    logout();
+  });
+
+const reloadPage = () => {
+  setTimeout(() => {
+    window.location.reload();
+  }, 1000);
+};
+// 閫�鍑�
+const logoutHandle = () => {
+  Modal.confirm({
+    title: t('login.logoutTip'),
+    onOk: () => {
+      logout();
+      Message.success(t('login.logoutSuccessTip'));
+      reloadPage();
+    },
+  });
+};
+// 鐧诲綍閫昏緫 ------------------
+const formInline = reactive({
+  identifier: '',
+  password: '',
+});
+const ruleInline = reactive({
+  identifier: [{ required: true, message: t('login.identifierValidate'), trigger: 'blur' }],
+  password: [
+    { required: true, message: t('login.passwordValidate'), trigger: 'blur' },
+    {
+      type: 'string',
+      min: 6,
+      message: t('login.passwordValidate'),
+      trigger: 'blur',
+    },
+  ],
+});
+const loginForm = ref(null);
+
+const loginHandle = () => {
+  loginForm.value.validate((valid) => {
+    if (valid) {
+      login(formInline)
+        .then((res) => {
+          setToken(res.data.jwt);
+          Message.success(t('login.welcome'));
+          reloadPage();
+        })
+        .catch((res) => {
+          Message.error(res.response.data.error.message);
+        });
+    }
+  });
+};
+
+// 娉ㄥ唽閫昏緫 鍒嗗壊绾�--------------------
+const registerFormInline = reactive({
+  username: '',
+  password: '',
+  email: '',
+});
+const registerRuleInline = reactive({
+  username: [{ required: true, message: t('login.identifierValidate'), trigger: 'blur' }],
+  password: [
+    { required: true, message: t('login.passwordValidate'), trigger: 'blur' },
+    {
+      type: 'string',
+      min: 6,
+      message: t('login.passwordValidate'),
+      trigger: 'blur',
+    },
+  ],
+  email: [
+    { required: true, message: t('login.emailValidate'), trigger: 'blur' },
+    {
+      type: 'email',
+      message: t('login.emailValidate'),
+      trigger: 'blur',
+    },
+  ],
+});
+const registerForm = ref(null);
+
+const registerHandle = () => {
+  registerForm.value.validate((valid) => {
+    if (valid) {
+      register(registerFormInline)
+        .then((res) => {
+          setToken(res.data.jwt);
+          Message.success(t('login.welcome'));
+          reloadPage();
+        })
+        .catch((res) => {
+          Message.error(res.response.data.error.message);
+        });
+    }
+  });
+};
+
+const switchTab = () => {
+  registerForm.value && registerForm.value.resetFields();
+  loginForm.value && loginForm.value.resetFields();
+};
+</script>
+<style scoped lang="less">
+.box {
+  display: inline-block;
+}
+.form-box {
+  padding-top: 10px;
+}
+h3 {
+  padding-bottom: 10px;
+}
+</style>
diff --git a/src/fabric-editor/components/logo.vue b/src/fabric-editor/components/logo.vue
new file mode 100644
index 0000000..5307f5f
--- /dev/null
+++ b/src/fabric-editor/components/logo.vue
@@ -0,0 +1,64 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-11 09:12:24
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-12 09:02:50
+ * @Description: logo
+-->
+
+<template>
+  <div class="logo">
+    <a :href="webInfo.url" target="_blank">
+      <img :src="webInfo.img" alt="webInfo.name" />
+    </a>
+  </div>
+</template>
+
+<script setup name="Logo">
+import { getWebInfo } from '@/api/material';
+import { get, pick } from 'lodash-es';
+const baseURL = import.meta.env.APP_APIHOST;
+
+const webInfo = ref({
+  name: '',
+  logo: '',
+  url: '',
+});
+
+const getWebInfoFun = async () => {
+  const res = await getWebInfo();
+  const info = pick(res.data.data.attributes, ['name', 'url']);
+  info.img = baseURL + get(res.data, 'data.attributes.logo.data.attributes.url');
+  webInfo.value = info;
+};
+
+getWebInfoFun();
+</script>
+<style scoped lang="less">
+.logo {
+  width: 117px;
+  height: 44px;
+  display: inline-block;
+  margin-right: 10px;
+  margin-left: 2px;
+  a {
+    display: flex;
+    height: 100%;
+    align-items: center;
+    img {
+      display: inline-block;
+      height: 80%;
+    }
+    span {
+      font-size: 18px;
+      font-weight: bold;
+      margin-left: 6px;
+    }
+  }
+  // text-align: center;
+  // vertical-align: middle;
+  // .ivu-icon {
+  //   vertical-align: super;
+  // }
+}
+</style>
diff --git a/src/fabric-editor/components/material.vue b/src/fabric-editor/components/material.vue
new file mode 100644
index 0000000..8dd70ef
--- /dev/null
+++ b/src/fabric-editor/components/material.vue
@@ -0,0 +1,125 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-17 15:31:24
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-07-06 17:16:52
+ * @Description: 绱犳潗鍒楄〃
+-->
+
+<template>
+  <div>
+    <!-- 鎼滅储缁勪欢 -->
+    <searchType
+      ref="selectTypeRef"
+      :typeListApi="getMaterialTypes"
+      @change="searchChange"
+    ></searchType>
+
+    <!-- 鍒嗙被鍒楄〃 -->
+    <typeList
+      v-show="!filters.material_type.$contains && !filters.name.$contains"
+      :typeApi="getMaterialTypes"
+      :typeListApi="getMaterialsByType"
+      typeKey="material_type"
+      :formatData="formatData"
+      @selectType="selectType"
+      @click="addItem"
+      @dragend="dragItem"
+    ></typeList>
+
+    <!-- 鎼滅储鍒嗛〉鍒楄〃 -->
+    <pageList
+      v-if="filters.material_type.$contains || filters.name.$contains"
+      DOMId="materialList"
+      :pageListApi="getMaterials"
+      :filters="filters"
+      @click="addItem"
+      :formatData="formatData"
+      @dragend="dragItem"
+    ></pageList>
+  </div>
+</template>
+
+<script setup name="ImportSvg">
+import searchType from '@/components/common/searchType';
+import typeList from '@/components/common/typeList.vue';
+import pageList from '@/components/common/pageList.vue';
+import useSelect from '@/hooks/select';
+import { getMaterialInfoUrl, getMaterialPreviewUrl } from '@/hooks/usePageList';
+import { getMaterialTypes, getMaterialsByType, getMaterials } from '@/api/material';
+import useCalculate from '@/hooks/useCalculate';
+import { useRoute } from 'vue-router';
+import { Utils } from '@kuaitu/core';
+
+const { canvasEditor } = useSelect();
+
+const { isOutsideCanvas } = useCalculate();
+const selectTypeRef = ref();
+
+const filters = reactive({
+  material_type: {
+    $contains: '',
+  },
+  name: {
+    $contains: '',
+  },
+});
+
+// 鍒嗛〉鏍煎紡鍖�
+const formatData = (data) => {
+  return data.map((item) => {
+    return {
+      id: item.id,
+      name: item.attributes.name,
+      desc: item.attributes.desc,
+      src: getMaterialInfoUrl(item.attributes.img),
+      previewSrc: getMaterialPreviewUrl(item.attributes.img),
+    };
+  });
+};
+
+// 鎼滅储鏀瑰彉
+const searchChange = async ({ searchKeyWord, typeValue }) => {
+  filters.name.$contains = '';
+  filters.material_type.$contains = '';
+  await nextTick();
+  filters.name.$contains = searchKeyWord;
+  filters.material_type.$contains = typeValue;
+};
+
+// 鍒嗙被鍒楄〃閫夋嫨
+const selectType = async (type) => {
+  filters.material_type.$contains = type;
+  selectTypeRef.value.setType(type);
+  await nextTick();
+};
+
+// 鎸夌収绫诲瀷娓叉煋
+const dragItem = async ({ e }) => {
+  if (isOutsideCanvas(e.clientX, e.clientY)) return;
+  const imgItem = await canvasEditor.createImgByElement(e.target);
+  canvasEditor.addBaseType(imgItem, {
+    scale: true,
+    event: e,
+  });
+};
+
+const addItem = async ({ e }) => {
+  const imgItem = await canvasEditor.createImgByElement(e.target);
+  canvasEditor.addBaseType(imgItem, {
+    scale: true,
+  });
+};
+
+onMounted(async () => {
+  // 榛樿娣诲姞鍥剧墖
+  const route = useRoute();
+  if (route?.query?.loadFile) {
+    const url = route.query.loadFile;
+    const image = await Utils.insertImgFile(url);
+    addItem({ target: image });
+  }
+});
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/myMaterial/components/file.vue b/src/fabric-editor/components/myMaterial/components/file.vue
new file mode 100644
index 0000000..cf487ba
--- /dev/null
+++ b/src/fabric-editor/components/myMaterial/components/file.vue
@@ -0,0 +1,196 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-30 10:48:00
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-09 17:08:48
+ * @Description: 妯℃澘鏂囦欢
+-->
+<template>
+  <Tooltip :content="props.name" placement="top">
+    <div class="file-type-box">
+      <Image
+        lazy
+        @click="beforeClearTip"
+        :src="props.src"
+        fit="contain"
+        height="100%"
+        width="100%"
+        :alt="props.name"
+      />
+      <div class="more">
+        <Dropdown trigger="click" @on-click="operation">
+          <Button size="small" shape="circle" type="text">
+            <Icon type="ios-more" :size="24" />
+          </Button>
+          <template #list>
+            <DropdownMenu>
+              <DropdownItem name="reName">閲嶅懡鍚�</DropdownItem>
+              <DropdownItem name="delete">鍒犻櫎</DropdownItem>
+              <DropdownItem name="transfer">杩佺Щ鐩綍</DropdownItem>
+            </DropdownMenu>
+          </template>
+        </Dropdown>
+      </div>
+    </div>
+  </Tooltip>
+  <!-- 杩佺Щ鏂囦欢澶� -->
+  <Modal v-model="modalVisable" title="璇烽�夋嫨杩佺Щ鐩綍" @on-ok="transferRequest">
+    <TreeSelect v-model="fileTypeId" :data="treeData" v-width="200" />
+  </Modal>
+  <!--  -->
+</template>
+
+<script setup name="ImportTmpl">
+import useMaterial from '@/hooks/useMaterial';
+import { useI18n } from 'vue-i18n';
+import useSelect from '@/hooks/select';
+import { getUserFileTypeTree, updataTempl } from '@/api/user';
+const { t } = useI18n();
+const { canvasEditor } = useSelect();
+const { reNameFileType, removeTemplInfo, routerToId, getTemplInfo } = useMaterial();
+
+import { Modal, Input, Spin, Message } from 'view-ui-plus';
+
+const props = defineProps({
+  name: {
+    type: String,
+    default: '',
+  },
+  src: {
+    type: String,
+    default: '',
+  },
+  previewSrc: {
+    type: String,
+    default: '',
+  },
+  itemId: {
+    type: [Number, String],
+    default: '',
+  },
+  json: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const emit = defineEmits(['change']);
+
+const operation = (value) => {
+  const mapActions = {
+    reName: reNameFile,
+    delete: deleteFile,
+    transfer: transfer,
+  };
+  mapActions[value]();
+};
+const fileName = ref('');
+
+const reNameFile = () => {
+  fileName.value = props.name;
+  Modal.confirm({
+    title: '閲嶅懡鍚�',
+    render: (h) => {
+      return h(Input, {
+        size: 'large',
+        modelValue: fileName,
+        autofocus: true,
+        placeholder: '璇疯緭鍏ユ枃浠跺悕绉�',
+      });
+    },
+    onOk: async () => {
+      if (fileName.value === '') {
+        Message.warning('鏂囦欢鍚嶇О涓嶈兘涓虹┖');
+        return;
+      }
+      await reNameFileType(fileName.value, props.itemId);
+      emit('change');
+    },
+  });
+};
+
+const deleteFile = async () => {
+  await removeTemplInfo(props.itemId);
+  emit('change');
+};
+
+const beforeClearTip = () => {
+  Modal.confirm({
+    title: t('tip'),
+    content: `<p>${t('replaceTip')}</p>`,
+    okText: t('ok'),
+    cancelText: t('cancel'),
+    onOk: () => getTempData(),
+  });
+};
+
+// 鑾峰彇妯℃澘鏁版嵁
+const getTempData = async () => {
+  Spin.show({
+    render: (h) => h('div', t('alert.loading_data')),
+  });
+  const data = await getTemplInfo(props.itemId);
+  routerToId(props.itemId);
+  canvasEditor.loadJSON(JSON.stringify(data.data.attributes.json), Spin.hide);
+};
+
+const modalVisable = ref(false);
+const fileTypeId = ref('');
+const treeData = ref([]);
+
+const transfer = async () => {
+  treeData.value = [];
+  fileTypeId.value = '';
+  const res = await getUserFileTypeTree();
+  treeData.value = [res.data.data];
+  modalVisable.value = true;
+};
+
+const transferRequest = async () => {
+  const parentId = fileTypeId.value === 'root' ? '' : fileTypeId.value;
+  await updataTempl(props.itemId, {
+    data: {
+      parentId: String(parentId),
+    },
+  });
+  emit('change');
+};
+</script>
+
+<style scoped lang="less">
+// 鏂囦欢澶�
+.file-type-box {
+  width: 134px;
+  height: 116px;
+  cursor: pointer;
+  background: #fff;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  border: 1px solid #f1f1f1;
+  border-radius: 10px;
+  img {
+    width: 60px;
+    margin-top: 12px;
+  }
+  div.more {
+    position: absolute;
+    display: none;
+  }
+  &:hover {
+    background: rgb(243, 245, 249);
+    border: 1px solid rgb(225, 230, 239);
+    border-radius: 8px;
+    img {
+      opacity: 0.8;
+    }
+    div.more {
+      display: block;
+      top: 5px;
+      right: 5px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/myMaterial/components/fileType.vue b/src/fabric-editor/components/myMaterial/components/fileType.vue
new file mode 100644
index 0000000..0f760c0
--- /dev/null
+++ b/src/fabric-editor/components/myMaterial/components/fileType.vue
@@ -0,0 +1,151 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-30 10:03:30
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-30 18:39:13
+ * @Description: 鏂囦欢澶�
+-->
+
+<template>
+  <div class="file-type-box">
+    <img :src="fileTypeIcon" />
+    <span>{{ props.name }}</span>
+    <div class="click-bg" @click="emit('select')"></div>
+    <div class="more">
+      <Dropdown trigger="click" @on-click="operation">
+        <Button size="small" shape="circle" type="text">
+          <Icon type="ios-more" :size="24" />
+        </Button>
+        <template #list>
+          <DropdownMenu>
+            <DropdownItem name="reName">閲嶅懡鍚�</DropdownItem>
+            <DropdownItem name="delete">鍒犻櫎</DropdownItem>
+          </DropdownMenu>
+        </template>
+      </Dropdown>
+    </div>
+  </div>
+</template>
+
+<script setup name="ImportTmpl">
+import fileTypeIcon from '@/assets/icon/fileType.png';
+import useMaterial from '@/hooks/useMaterial';
+
+const { reNameFileType, removeFileType } = useMaterial();
+
+import { Modal, Input, Message } from 'view-ui-plus';
+
+const props = defineProps({
+  name: {
+    type: String,
+    default: '',
+  },
+  itemId: {
+    type: [Number, String],
+    default: '',
+  },
+});
+
+const emit = defineEmits(['change', 'select']);
+
+const operation = (value) => {
+  const mapActions = {
+    reName: reNameFile,
+    delete: deleteFile,
+  };
+  mapActions[value]();
+};
+const fileName = ref('');
+
+const reNameFile = () => {
+  fileName.value = props.name;
+  Modal.confirm({
+    title: '閲嶅懡鍚嶆枃浠跺す',
+    render: (h) => {
+      return h(Input, {
+        size: 'large',
+        modelValue: fileName,
+        autofocus: true,
+        placeholder: '璇疯緭鍏ユ枃浠跺す鍚嶇О',
+      });
+    },
+    onOk: async () => {
+      if (fileName.value === '') {
+        Message.warning('鏂囦欢澶瑰悕绉颁笉鑳戒负绌�');
+        return;
+      }
+      await reNameFileType(fileName.value, props.itemId);
+      emit('change');
+    },
+  });
+};
+
+const deleteFile = async () => {
+  const res = await removeFileType(props.itemId);
+  if (res?.data?.msg) {
+    Message.error({
+      content: res.data?.msg,
+      duration: 3,
+    });
+    return;
+  }
+  emit('change');
+};
+</script>
+
+<style scoped lang="less">
+// 鏂囦欢澶�
+.file-type-box {
+  width: 134px;
+  height: 116px;
+  cursor: pointer;
+  background: #fff;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  // border: 1px solid #fff;
+  border: 1px solid #f1f1f1;
+  border-radius: 10px;
+  img {
+    width: 60px;
+    margin-top: 12px;
+  }
+  span {
+    display: block;
+    padding-top: 10px;
+    text-align: center;
+    width: 90%;
+    overflow: hidden; //瓒呭嚭鐨勬枃鏈殣钘�
+    text-overflow: ellipsis; //婧㈠嚭鐢ㄧ渷鐣ュ彿鏄剧ず
+    white-space: nowrap; //婧㈠嚭涓嶆崲琛�
+  }
+  div.more {
+    position: absolute;
+    display: none;
+  }
+
+  .click-bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+  }
+
+  &:hover {
+    background: rgb(243, 245, 249);
+    border: 1px solid rgb(225, 230, 239);
+    border-radius: 8px;
+    img {
+      opacity: 0.8;
+    }
+    div.more {
+      display: block;
+      top: 5px;
+      right: 5px;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/myMaterial/index.vue b/src/fabric-editor/components/myMaterial/index.vue
new file mode 100644
index 0000000..20a16be
--- /dev/null
+++ b/src/fabric-editor/components/myMaterial/index.vue
@@ -0,0 +1,49 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-04-25 15:30:54
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-30 11:53:28
+ * @Description: 鎴戠殑绱犳潗
+-->
+
+<template>
+  <div class="my-material" v-if="isLogin">
+    <Tabs v-model="type">
+      <TabPane label="妯℃澘" name="templ">
+        <myTempl v-if="type === 'templ'"></myTempl>
+      </TabPane>
+      <TabPane label="鍥剧墖" name="img">
+        <uploadMaterial v-if="type === 'img'"></uploadMaterial>
+      </TabPane>
+    </Tabs>
+  </div>
+  <div class="tip" v-else>璇峰厛鐧诲綍</div>
+</template>
+
+<script setup name="ImportTmpl">
+import { getFileList } from '@/api/user';
+import uploadMaterial from './uploadMaterial';
+import myTempl from './myTempl';
+
+const type = ref('templ');
+const isLogin = ref(false);
+const getFileListHandle = () => {
+  // 鑾峰彇绱犳潗鍒楄〃
+  getFileList()
+    .then(() => {
+      isLogin.value = true;
+    })
+    .catch(() => {
+      isLogin.value = false;
+    });
+};
+
+getFileListHandle();
+</script>
+
+<style scoped lang="less">
+.tip {
+  padding: 20px;
+  text-align: center;
+}
+</style>
diff --git a/src/fabric-editor/components/myMaterial/myTempl.vue b/src/fabric-editor/components/myMaterial/myTempl.vue
new file mode 100644
index 0000000..c846acb
--- /dev/null
+++ b/src/fabric-editor/components/myMaterial/myTempl.vue
@@ -0,0 +1,253 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: June 1601745371@qq.com
+ * @LastEditTime: 2024-06-12 14:20:11
+ * @Description: 瀵煎叆妯℃澘
+-->
+
+<template>
+  <div>
+    <!-- 鎼滅储缁勪欢 -->
+    <div class="search-box">
+      <Dropdown @on-click="createType" placement="bottom-start" style="margin-right: 10px" transfer>
+        <Button type="primary" icon="md-add"></Button>
+        <template #list>
+          <DropdownMenu>
+            <DropdownItem name="file">鏂板缓璁捐</DropdownItem>
+            <DropdownItem name="fileType">鏂板缓鏂囦欢澶�</DropdownItem>
+          </DropdownMenu>
+        </template>
+      </Dropdown>
+
+      <Input
+        class="input"
+        placeholder="璇疯緭鍏ュ叧閿瘝"
+        v-model="filters.name.$contains"
+        search
+        :disabled="pageLoading"
+        @on-search="startGetList"
+      />
+    </div>
+
+    <!-- 闈㈠寘灞戝鑸� -->
+    <Breadcrumb>
+      <BreadcrumbItem
+        @click="toFile(item.parentId, i)"
+        :key="item.id"
+        v-for="(item, i) in filePath"
+      >
+        {{ item.name }}
+      </BreadcrumbItem>
+    </Breadcrumb>
+
+    <!-- 鍒楄〃 -->
+    <div style="height: calc(100vh - 160px)" id="myFileTemplBox">
+      <Scroll
+        key="myFileTemplBox"
+        v-if="showScroll"
+        :on-reach-bottom="nextPage"
+        :height="scrollHeight"
+        :distance-to-edge="[-1, -1]"
+      >
+        <!-- 鍒楄〃 -->
+        <div v-for="info in pageData" :key="info.name" class="item">
+          <!-- 鏂囦欢澶规牱寮� -->
+          <fileType
+            v-if="info.type === 'fileType'"
+            :itemId="info.id"
+            :name="info.name"
+            @select="() => joinFileTyper(info.id, info.name)"
+            @change="startGetList"
+          ></fileType>
+
+          <file
+            v-else
+            :src="info.src"
+            :json="info.json"
+            :previewSrc="info.previewSrc"
+            :itemId="info.id"
+            :name="info.name"
+            @change="startGetList"
+          ></file>
+        </div>
+        <Spin size="large" fix :show="pageLoading"></Spin>
+        <Divider plain v-if="isDownBottom">宸茬粡鍒板簳浜�</Divider>
+      </Scroll>
+    </div>
+
+    <!-- 鍒涘缓璁捐 -->
+    <modalSzie
+      :title="$t('importFiles.createDesign.title')"
+      ref="modalSizeRef"
+      @set="customSizeCreate"
+    ></modalSzie>
+  </div>
+</template>
+
+<script setup name="ImportTmpl">
+import { Input } from 'view-ui-plus';
+import { Spin, Modal, Message } from 'view-ui-plus';
+
+// 缁勪欢
+import fileType from './components/fileType.vue';
+import file from './components/file.vue';
+import modalSzie from '@/components/common/modalSzie';
+
+// API
+import { getTmplList, getFileTypeTree } from '@/api/user';
+// 绱犳潗涓庡垎椤�
+import useMaterial from '@/hooks/useMaterial';
+import usePageList, { getMaterialInfoUrl, getMaterialPreviewUrl } from '@/hooks/usePageList';
+// 璺敱
+import { useRoute } from 'vue-router';
+const route = useRoute();
+
+// 鐢ㄦ埛绱犳潗API鎿嶄綔
+const { createdFileType, createTmpl } = useMaterial();
+
+// 妫�绱㈡潯浠�
+const filters = reactive({
+  name: {
+    $contains: '',
+  },
+  parentId: {
+    $eq: '',
+    filterEmpty: false,
+  },
+});
+
+const sort = ['type:desc'];
+
+// 鍒嗛〉鏍煎紡鍖�
+const formatData = (data) => {
+  return data.map((item) => {
+    return {
+      id: item.id,
+      name: item.attributes.name,
+      type: item.attributes.type || 'file',
+      desc: item.attributes.desc,
+      json: item.attributes.json,
+      src: getMaterialInfoUrl(item.attributes.img),
+      previewSrc: getMaterialPreviewUrl(item.attributes.img),
+    };
+  });
+};
+// 閫氱敤鍒嗛〉
+const {
+  pageData,
+  showScroll,
+  scrollHeight,
+  isDownBottom,
+  pageLoading,
+  startPage,
+  startGetList,
+  nextPage,
+} = usePageList({
+  el: '#myFileTemplBox',
+  apiClient: getTmplList,
+  filters,
+  sort,
+  fields: ['name', 'parentId', 'type', 'externalId'],
+  formatData,
+});
+
+onMounted(async () => {
+  await getFileTypeTreeData();
+  startPage();
+});
+
+const fileTypeName = ref('');
+const modalSizeRef = ref(null);
+// 鏂板缓鏂囦欢
+const createType = (type) => {
+  if (type === 'fileType') {
+    fileTypeName.value = '';
+    Modal.confirm({
+      title: '鏂板缓鏂囦欢澶�',
+      render: (h) => {
+        return h(Input, {
+          size: 'large',
+          modelValue: fileTypeName,
+          autofocus: true,
+          placeholder: '璇疯緭鍏ユ枃浠跺す鍚嶇О',
+        });
+      },
+      onOk: async () => {
+        if (fileTypeName.value === '') {
+          Message.warning('鏂囦欢澶瑰悕绉颁笉鑳戒负绌�');
+          return;
+        }
+        await createdFileType(fileTypeName.value, filters.parentId.$eq);
+        startGetList();
+      },
+    });
+  } else {
+    modalSizeRef.value.showSetSize();
+  }
+};
+const customSizeCreate = async (w, h) => {
+  await createTmpl(w, h, filters.parentId.$eq);
+  startGetList();
+};
+
+const filePath = ref([
+  {
+    name: '鍏ㄩ儴',
+    parentId: '',
+  },
+]);
+
+const getFileTypeTreeData = async () => {
+  if (route?.query?.id) {
+    const res = await getFileTypeTree({
+      id: route.query.id,
+    });
+    filePath.value = res.data.data;
+    const last = res.data.data[res.data.data.length - 1];
+    filters.parentId.$eq = last.parentId;
+  }
+};
+// 杩涘叆鏂囦欢澶�
+const joinFileTyper = (id, name) => {
+  filters.parentId.$eq = String(id);
+  filePath.value.push({
+    name: name,
+    parentId: id,
+  });
+  startGetList();
+};
+
+// 闈㈠寘灞戣烦杞枃浠跺す
+const toFile = (id, i) => {
+  const isLast = i === filePath.value.length - 1;
+  if (!isLast) {
+    filters.parentId.$eq = id;
+    filePath.value = filePath.value.slice(0, i + 1);
+    startGetList();
+  }
+};
+</script>
+
+<style scoped lang="less">
+.search-box {
+  padding-bottom: 10px;
+  display: flex;
+}
+
+:deep(.ivu-scroll-content) {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  padding-right: 10px;
+}
+.item {
+  margin-bottom: 10px;
+}
+:deep(.ivu-breadcrumb-item-link) {
+  cursor: pointer;
+  &:hover {
+    color: #57a3f3;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/myMaterial/uploadMaterial.vue b/src/fabric-editor/components/myMaterial/uploadMaterial.vue
new file mode 100644
index 0000000..aacdca9
--- /dev/null
+++ b/src/fabric-editor/components/myMaterial/uploadMaterial.vue
@@ -0,0 +1,149 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-04-25 15:30:54
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-07-06 17:13:28
+ * @Description: 鎴戠殑绱犳潗
+-->
+
+<template>
+  <div class="my-material">
+    <Button icon="md-cloud-upload" @click="uploadImgHandule" long type="primary">
+      {{ $t('myMaterial.uploadBtn') }}
+    </Button>
+    <div class="img-group" v-if="fileList.length">
+      <Tooltip
+        :content="info.name"
+        v-for="(info, i) in fileList"
+        :key="`${i}-bai1-button`"
+        placement="top"
+      >
+        <div class="tmpl-img-box">
+          <Icon
+            type="ios-trash"
+            class="del-btn"
+            color="red"
+            @click="removeMaterialHandle(info.id)"
+          />
+          <Image
+            lazy
+            :src="info.imgUrl"
+            fit="contain"
+            height="100%"
+            :alt="info.name"
+            @click="addImgByElement"
+          />
+        </div>
+      </Tooltip>
+    </div>
+    <div class="tip" v-else>鏆傛棤绱犳潗</div>
+  </div>
+</template>
+
+<script setup name="ImportTmpl">
+const APP_APIHOST = import.meta.env.APP_APIHOST;
+import { getFileList, uploadImg, createdMaterial, removeMaterial } from '@/api/user';
+import { Utils } from '@kuaitu/core';
+const { selectFiles } = Utils;
+const canvasEditor = inject('canvasEditor');
+
+const fileList = ref([]);
+const isLogin = ref(false);
+const getFileListHandle = () => {
+  // 鑾峰彇绱犳潗鍒楄〃
+  getFileList()
+    .then((res) => {
+      fileList.value = res.data.data.map((item) => {
+        return {
+          id: item.id,
+          name: item.attributes.name,
+          imgUrl: APP_APIHOST + item.attributes.img.data.attributes.url,
+        };
+      });
+      isLogin.value = true;
+    })
+    .catch(() => {
+      isLogin.value = false;
+    });
+};
+
+getFileListHandle();
+
+// 涓婁紶绱犳潗
+const uploadImgHandule = () => {
+  selectFiles({
+    accept: 'image/*',
+  }).then((fileList) => {
+    const formData = new FormData();
+    const time = new Date();
+    const [file] = fileList;
+    formData.append('files', file, `${time.getTime()}`);
+    uploadImg(formData)
+      .then((res) => {
+        const [info] = res.data;
+        createdH(info.id, file.name);
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  });
+};
+// 鍒涘缓绱犳潗
+const createdH = (id, fileName) => {
+  createdMaterial({
+    data: {
+      img: id,
+      name: fileName,
+    },
+  }).finally(getFileListHandle);
+};
+// 娣诲姞绱犳潗鍒扮敾甯�
+const addImgByElement = async (e) => {
+  const imgItem = await canvasEditor.createImgByElement(e.target);
+  canvasEditor.addBaseType(imgItem, {
+    scale: true,
+  });
+};
+
+// 鍒犻櫎绱犳潗
+const removeMaterialHandle = (id) => {
+  removeMaterial(id).finally(getFileListHandle);
+};
+</script>
+
+<style scoped lang="less">
+.img-group {
+  background: #eeeeeea1;
+  border-radius: 10px;
+  padding: 10px;
+  margin-top: 10px;
+}
+.tmpl-img-box {
+  width: 134px;
+  height: 180px;
+  padding: 5px;
+  cursor: pointer;
+  border-radius: 10px;
+  text-align: center;
+
+  &:hover {
+    background: #e3e3e3;
+    .del-btn {
+      // opacity: 1;
+      right: 5px;
+    }
+  }
+}
+
+.del-btn {
+  z-index: 1;
+  position: absolute;
+  top: 5px;
+  right: 1000000px;
+}
+
+.tip {
+  text-align: center;
+  padding: 10px;
+}
+</style>
diff --git a/src/fabric-editor/components/myTemplName.vue b/src/fabric-editor/components/myTemplName.vue
new file mode 100644
index 0000000..b0420e1
--- /dev/null
+++ b/src/fabric-editor/components/myTemplName.vue
@@ -0,0 +1,137 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-11 13:23:48
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-30 18:49:59
+ * @Description: 鏂囦欢鍚嶇О
+-->
+
+<template>
+  <div style="display: inline-block" v-if="route?.query?.id">
+    <Input
+      v-model="fileName"
+      placeholder="璇疯緭鍏ユ枃浠跺悕绉�"
+      style="width: 190px"
+      size="small"
+      @on-change="changeFileName"
+      :loading="loading"
+    />
+    <Button type="text" @click="saveTempl" :loading="loading">
+      <Icon type="ios-cloud-done" :color="loading ? '#ff9900' : '#19be6b'" />
+    </Button>
+    <Divider type="vertical" />
+  </div>
+</template>
+
+<script name="ImportJson" setup>
+import { debounce } from 'lodash-es';
+import { Message } from 'view-ui-plus';
+import useMaterial from '@/hooks/useMaterial';
+import { useRoute, useRouter } from 'vue-router';
+import useSelect from '@/hooks/select';
+import { getTmplList } from '@/api/user';
+
+// const APIHOST = import.meta.env.APP_APIHOST;
+import qs from 'qs';
+const router = useRouter();
+const { getTemplInfo, updataTemplInfo } = useMaterial();
+
+const { canvasEditor } = useSelect();
+
+const fileName = ref('');
+const route = useRoute();
+const loading = ref(false);
+
+watch(
+  () => route.query,
+  (query) => {
+    if (query?.id) {
+      getTemplInfo(query?.id).then((res) => {
+        fileName.value = res?.data?.attributes?.name;
+      });
+    } else {
+      fileName.value = false;
+    }
+  }
+);
+
+onMounted(async () => {
+  if (route?.query?.id) {
+    getTemplInfo(route?.query?.id)
+      .then((res) => {
+        fileName.value = res?.data?.attributes?.name;
+        canvasEditor.loadJSON(JSON.stringify(res?.data?.attributes?.json));
+      })
+      .catch(() => {
+        window.location.href = '/';
+      });
+  } else if (route?.query?.projectid) {
+    const infoid = await queryTemplIdByProId(route?.query?.projectid);
+    if (infoid) {
+      getTemplInfo(infoid).then((res) => {
+        router.replace(route.fullPath + '&id=' + infoid);
+        fileName.value = res?.data?.attributes?.name;
+        canvasEditor.loadJSON(JSON.stringify(res?.data?.attributes?.json));
+      });
+    }
+  }
+});
+
+const queryTemplIdByProId = (projectid) => {
+  const query = {
+    populate: {
+      img: '*',
+    },
+    filters: {
+      externalId: {
+        $eq: String(projectid),
+      },
+    },
+    pagination: {
+      page: 1,
+      pageSize: 50,
+    },
+  };
+
+  return getTmplList(qs.stringify(query))
+    .then((res) => {
+      if (res.data.data.length) {
+        return res.data.data[0].id;
+      }
+    })
+    .catch((err) => {
+      return err;
+    });
+};
+
+const saveTempl = () => {
+  if (fileName.value === '') {
+    Message.warning('鏂囦欢鍚嶇О涓嶈兘涓虹┖');
+    fileName.value = '榛樿鏂囦欢鍚嶇О';
+    return;
+  }
+  loading.value = true;
+  updataTemplInfo(route.query.id, fileName.value)
+    .then()
+    .finally(() => {
+      loading.value = false;
+    });
+};
+
+// canvasEditor
+const changeFileName = debounce(() => {
+  if (route.query.id) {
+    saveTempl();
+  }
+}, 3000);
+// 鑷姩淇濆瓨浼樺寲
+canvasEditor.canvas.on('object:modified', changeFileName);
+</script>
+<style scoped lang="less">
+h3 {
+  margin-bottom: 10px;
+}
+.divider {
+  margin-top: 0;
+}
+</style>
diff --git a/src/fabric-editor/components/previewCurrent.vue b/src/fabric-editor/components/previewCurrent.vue
new file mode 100644
index 0000000..d38b340
--- /dev/null
+++ b/src/fabric-editor/components/previewCurrent.vue
@@ -0,0 +1,29 @@
+<!--
+ * @Author: bigFace2019 599069310@qq.com
+ * @Date: 2023-04-09 11:19:07
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2023-07-05 00:58:02
+ * @FilePath: \vue-fabric-editor\src\components\preview.vue
+ * @Description: 棰勮缁勪欢
+-->
+<template>
+  <Button type="text" @click="preview">
+    {{ $t('preview') }}
+  </Button>
+</template>
+
+<script lang="ts" setup>
+import { ImagePreview } from 'view-ui-plus';
+
+const canvasEditor: any = inject('canvasEditor');
+const preview = () => {
+  canvasEditor.preview().then((dataUrl: string) => {
+    // const dataUrl = getImgUrl();
+    ImagePreview.show({
+      previewList: [dataUrl],
+    });
+  });
+};
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/previewImageList.vue b/src/fabric-editor/components/previewImageList.vue
new file mode 100644
index 0000000..d9286c9
--- /dev/null
+++ b/src/fabric-editor/components/previewImageList.vue
@@ -0,0 +1,72 @@
+<template>
+  <div v-if="templateImageList.length > 0" class="fabric-preview-image-list">
+    <el-image
+      v-for="(image, index) in templateImageList"
+      :key="image.path"
+      :src="image.path"
+      :class="[
+        'fabric-preview-image-list-item',
+        { active: templateEditState.currentImageIndex === index },
+      ]"
+      fit="cover"
+      @click="handleClick(index)"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useTemplateDetailContext, useCanvasActions } from '../hooks/context';
+import { setOSSLink } from '@/utils';
+import useSelect from '../hooks/select';
+
+defineOptions({
+  name: 'previewImageList',
+});
+
+const { fabric, canvasEditor } = useSelect();
+
+const { templateImageList, templateEditState, templateDetail } = useTemplateDetailContext();
+
+const { toggleCanvas, setBackgroundImage, clear } = useCanvasActions();
+
+watch(templateDetail, async (newValue, oldValue) => {
+  if (!!newValue.id && !oldValue.id) {
+    if (newValue.templateJsonData && templateEditState.jsonMap[0]) {
+      canvasEditor.loadJSON(templateEditState.jsonMap[0], () => {
+        canvasEditor.canvas.discardActiveObject();
+      });
+      // canvasEditor.canvas.loadFromJSON(templateEditState.jsonMap[0], () => {});
+    } else {
+      if (templateImageList.value.length > 0) {
+        setBackgroundImage(templateImageList.value[0].path);
+      }
+    }
+  }
+});
+
+function handleClick(index: number) {
+  toggleCanvas(index);
+}
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+
+.fabric-preview-image-list {
+  .fabric-preview-image-list-item {
+    margin-bottom: 10px;
+    width: 100%;
+    height: 300px;
+    border: 1px solid getCssVar('border-color', 'lighter');
+    cursor: pointer;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    &.active {
+      border-color: getCssVar('color', 'primary');
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/replaceImg.vue b/src/fabric-editor/components/replaceImg.vue
new file mode 100644
index 0000000..2575dac
--- /dev/null
+++ b/src/fabric-editor/components/replaceImg.vue
@@ -0,0 +1,71 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2023-04-06 22:26:57
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-10-07 17:15:32
+ * @Description: 鍥剧墖鏇挎崲
+-->
+
+<template>
+  <div v-if="isOne && type === 'image'" class="attr-item-box">
+    <div class="bg-item">
+      <Button @click="repleace" type="text" long>{{ $t('repleaceImg') }}</Button>
+    </div>
+  </div>
+</template>
+
+<script setup name="ReplaceImg">
+import useSelect from '@/hooks/select';
+
+import { Utils } from '@kuaitu/core';
+const { getImgStr, selectFiles, insertImgFile } = Utils;
+
+const update = getCurrentInstance();
+// const canvasEditor = inject('canvasEditor');
+const { canvasEditor, isOne } = useSelect();
+const type = ref('');
+
+// 鏇挎崲鍥剧墖
+const repleace = async () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject && activeObject.type === 'image') {
+    // 鍥剧墖
+    const [file] = await selectFiles({ accept: 'image/*', multiple: false });
+    // 杞瓧绗︿覆
+    const fileStr = await getImgStr(file);
+    // 瀛楃涓茶浆El
+    const imgEl = await insertImgFile(fileStr);
+    const width = activeObject.get('width');
+    const height = activeObject.get('height');
+    const scaleX = activeObject.get('scaleX');
+    const scaleY = activeObject.get('scaleY');
+    activeObject.setSrc(imgEl.src, () => {
+      activeObject.set('scaleX', (width * scaleX) / imgEl.width);
+      activeObject.set('scaleY', (height * scaleY) / imgEl.height);
+      canvasEditor.canvas.renderAll();
+    });
+    imgEl.remove();
+  }
+};
+
+const init = () => {
+  const activeObject = canvasEditor.canvas.getActiveObjects()[0];
+  if (activeObject) {
+    type.value = activeObject.type;
+    update?.proxy?.$forceUpdate();
+  }
+};
+
+onMounted(() => {
+  canvasEditor.on('selectOne', init);
+});
+
+onBeforeUnmount(() => {
+  canvasEditor.off('selectOne', init);
+});
+</script>
+<style lang="less" scoped>
+.attr-item-box {
+  margin-top: 8px;
+}
+</style>
diff --git a/src/fabric-editor/components/save.vue b/src/fabric-editor/components/save.vue
new file mode 100644
index 0000000..6221501
--- /dev/null
+++ b/src/fabric-editor/components/save.vue
@@ -0,0 +1,112 @@
+<template>
+  <div class="save-box">
+    <Button style="margin-left: 10px" type="text" @click="beforeClear">
+      {{ $t('save.empty') }}
+    </Button>
+    <Dropdown style="margin-left: 10px" @on-click="saveWith">
+      <Button type="primary">
+        {{ $t('save.down') }}
+        <Icon type="ios-arrow-down"></Icon>
+      </Button>
+      <template #list>
+        <DropdownMenu>
+          <DropdownItem name="saveMyClould">{{ $t('save.save_my_spase') }}</DropdownItem>
+          <DropdownItem name="saveImg" divided>{{ $t('save.save_as_picture') }}</DropdownItem>
+          <DropdownItem name="saveSvg">{{ $t('save.save_as_svg') }}</DropdownItem>
+          <DropdownItem name="clipboard" divided>{{ $t('save.copy_to_clipboard') }}</DropdownItem>
+          <DropdownItem name="clipboardBase64">{{ $t('save.copy_to_clipboardstr') }}</DropdownItem>
+          <DropdownItem name="saveJson" divided>{{ $t('save.save_as_json') }}</DropdownItem>
+        </DropdownMenu>
+      </template>
+    </Dropdown>
+  </div>
+</template>
+
+<script setup name="save-bar">
+import { Modal } from 'view-ui-plus';
+import useSelect from '@/hooks/select';
+import useMaterial from '@/hooks/useMaterial';
+import { debounce } from 'lodash-es';
+import { useI18n } from 'vue-i18n';
+import { Spin } from 'view-ui-plus';
+import { useRoute } from 'vue-router';
+import { Message } from 'view-ui-plus';
+const route = useRoute();
+
+const { createTmplByCommon, updataTemplInfo, routerToId } = useMaterial();
+
+const { t } = useI18n();
+
+const { canvasEditor } = useSelect();
+const cbMap = {
+  async clipboard() {
+    try {
+      await canvasEditor.clipboard();
+      Message.success('澶嶅埗鎴愬姛');
+    } catch (error) {
+      Message.error('澶嶅埗澶辫触');
+    }
+  },
+  saveJson() {
+    canvasEditor.saveJson();
+  },
+  saveSvg() {
+    canvasEditor.saveSvg();
+  },
+  saveImg() {
+    canvasEditor.saveImg();
+  },
+  async clipboardBase64() {
+    try {
+      await canvasEditor.clipboardBase64();
+      Message.success('澶嶅埗鎴愬姛');
+    } catch (error) {
+      Message.error('澶嶅埗澶辫触');
+    }
+  },
+  async saveMyClould() {
+    try {
+      Spin.show();
+      if (route?.query?.id) {
+        await updataTemplInfo(route?.query?.id);
+      } else {
+        const res = await createTmplByCommon();
+        routerToId(res.data.data.id);
+      }
+    } catch (error) {
+      Message.warning('璇风櫥褰�');
+    }
+    Spin.hide();
+  },
+};
+
+const saveWith = debounce(function (type) {
+  cbMap[type] && typeof cbMap[type] === 'function' && cbMap[type]();
+}, 300);
+
+/**
+ * @desc clear canvas 娓呯┖鐢诲竷
+ */
+const clear = () => {
+  canvasEditor.clear();
+  canvasEditor.canvas.clearHistory(false);
+  canvasEditor.historyUpdate();
+};
+
+const beforeClear = () => {
+  Modal.confirm({
+    title: t('tip'),
+    content: `<p>${t('clearTip')}</p>`,
+    okText: t('ok'),
+    cancelText: t('cancel'),
+    onOk: () => clear(),
+  });
+};
+</script>
+
+<style scoped lang="less">
+.save-box {
+  display: inline-block;
+  padding-right: 10px;
+}
+</style>
diff --git a/src/fabric-editor/components/saveV2.vue b/src/fabric-editor/components/saveV2.vue
new file mode 100644
index 0000000..ba9470e
--- /dev/null
+++ b/src/fabric-editor/components/saveV2.vue
@@ -0,0 +1,29 @@
+<template>
+  <div class="save-box">
+    <!-- <el-button type="primary" size="small" @click="exportCanvas" class="save-btn">瀵煎嚭</el-button> -->
+    <el-button type="primary" size="small" @click="saveAllCanvas" class="save-btn">淇濆瓨</el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useCanvasActions } from '../hooks/context';
+
+defineOptions({
+  name: 'saveV2',
+});
+
+const { saveAllCanvas, exportCanvas } = useCanvasActions();
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+
+.save-box {
+  display: inline-block;
+  padding-right: 10px;
+
+  .save-btn {
+    height: auto !important;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/setSize.vue b/src/fabric-editor/components/setSize.vue
new file mode 100644
index 0000000..c3f4dcd
--- /dev/null
+++ b/src/fabric-editor/components/setSize.vue
@@ -0,0 +1,82 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-09-03 19:16:55
+ * @LastEditors: June
+ * @LastEditTime: 2024-11-22 15:28:43
+ * @Description: 灏哄璁剧疆
+-->
+
+<template>
+  <div v-if="!isSelect" class="attr-item-box">
+    <!-- <h3>{{ $t('bgSeting.size') }}</h3> -->
+    <Divider plain orientation="left">
+      <h4>{{ $t('bgSeting.size') }}</h4>
+    </Divider>
+    <Form :label-width="40" inline class="form-wrap">
+      <FormItem :label="$t('bgSeting.width')" prop="name">
+        <InputNumber disabled v-model="width" readonly @on-change="setSize"></InputNumber>
+      </FormItem>
+      <FormItem :label="$t('bgSeting.height')" prop="name">
+        <InputNumber disabled v-model="height" readonly @on-change="setSize"></InputNumber>
+      </FormItem>
+      <FormItem :label-width="0">
+        <Button type="text" @click="showSetSize">
+          <Icon type="md-create" />
+        </Button>
+      </FormItem>
+    </Form>
+
+    <!-- <Divider plain></Divider> -->
+    <!-- 淇敼灏哄 -->
+    <modalSzie :title="$t('setSizeTip')" ref="modalSizeRef" @set="handleConfirm"></modalSzie>
+  </div>
+</template>
+
+<script setup name="CanvasSize">
+import useSelect from '@/hooks/select';
+import modalSzie from '@/components/common/modalSzie';
+
+const { isSelect, canvasEditor } = useSelect();
+
+const modalSizeRef = ref(null);
+
+const width = ref(0);
+const height = ref(0);
+
+onMounted(() => {
+  const size = canvasEditor.getWorkspase();
+  const { width: w, height: h } = size || {};
+  width.value = w;
+  height.value = h;
+  canvasEditor.on('sizeChange', (w, h) => {
+    width.value = w;
+    height.value = h;
+  });
+});
+
+const setSize = () => {
+  canvasEditor.setSize(width.value, height.value);
+};
+
+const showSetSize = () => {
+  modalSizeRef.value.showSetSize(width.value, height.value);
+};
+const handleConfirm = (w, h) => {
+  width.value = w;
+  height.value = h;
+  setSize();
+};
+</script>
+
+<style scoped lang="less">
+:deep(.ivu-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.ivu-input-number) {
+  width: 70px;
+}
+.form-wrap {
+  display: flex;
+}
+</style>
diff --git a/src/fabric-editor/components/svgIcon/index.js b/src/fabric-editor/components/svgIcon/index.js
new file mode 100644
index 0000000..fa31749
--- /dev/null
+++ b/src/fabric-editor/components/svgIcon/index.js
@@ -0,0 +1,15 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-04-20 17:13:36
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2022-04-20 17:19:46
+ * @Description: file content
+ */
+
+import svgIcon from './index.vue';
+
+export default {
+  install(Vue) {
+    Vue.component(svgIcon.name, svgIcon);
+  },
+};
diff --git a/src/fabric-editor/components/svgIcon/index.vue b/src/fabric-editor/components/svgIcon/index.vue
new file mode 100644
index 0000000..f47f7b6
--- /dev/null
+++ b/src/fabric-editor/components/svgIcon/index.vue
@@ -0,0 +1,63 @@
+<template>
+  <span>
+    <svg
+      t="1650443094178"
+      viewBox="0 0 1024 1024"
+      version="1.1"
+      xmlns="http://www.w3.org/2000/svg"
+      p-id="1549"
+      :width="size"
+      :height="size"
+      :fill="color"
+    >
+      <!--  -->
+      <template v-if="name === 'default'">
+        <path
+          d="M252.76928 299.904l146.2784 0 0 472.42752-146.2784 0 0-472.42752Z"
+          p-id="1550"
+        ></path>
+        <path
+          d="M477.48096 85.34528l70.87104 0 0 885.80608-70.87104 0 0-885.80608Z"
+          p-id="1551"
+        ></path>
+        <path
+          d="M629.80096 284.8l31.0016 0 0 502.88128-31.0016 0L629.80096 284.8zM776.42752 284.8l31.0016 0 0 502.88128-31.0016 0L776.42752 284.8zM657.09056 315.8016l0-31.0016 123.04896 0 0 31.0016L657.09056 315.8016zM657.27488 787.64544l0-31.0016 123.04896 0 0 31.0016L657.27488 787.64544z"
+          p-id="1552"
+        ></path>
+      </template>
+      <!-- up -->
+      <template v-if="name === 'up'">
+        <path
+          d="M876.2 434.3L536.7 94.9c-6.6-6.6-15.5-10.3-24.7-10.3-9.3 0-18.2 3.7-24.7 10.3L147.8 434.3c-13.7 13.7-13.7 35.8 0 49.5 13.7 13.7 35.8 13.7 49.5 0L477 204.1v700.2c0 19.3 15.7 35 35 35s35-15.7 35-35V204.1l279.7 279.7c6.8 6.8 15.8 10.3 24.7 10.3s17.9-3.4 24.7-10.3c13.7-13.7 13.7-35.8 0.1-49.5z"
+          p-id="1800"
+        ></path>
+      </template>
+    </svg>
+  </span>
+</template>
+
+<script>
+export default {
+  name: 'svgIcon',
+  props: {
+    name: {
+      type: String,
+      default: 'default',
+    },
+    color: {
+      type: String,
+      default: '#ea9518',
+    },
+    size: {
+      type: Number,
+      default: 14,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style scoped lang="less"></style>
diff --git a/src/fabric-editor/components/systemTemplateDataParamSetting.vue b/src/fabric-editor/components/systemTemplateDataParamSetting.vue
new file mode 100644
index 0000000..7ea1a8c
--- /dev/null
+++ b/src/fabric-editor/components/systemTemplateDataParamSetting.vue
@@ -0,0 +1,187 @@
+<template>
+  <div>
+    <el-divider content-position="left">绯荤粺妯℃澘鍙傛暟</el-divider>
+    <div class="tool-box">
+      <el-tag
+        v-for="(item, index) in templateParamList"
+        :key="item.code"
+        @click="() => addTemplateParam(item)"
+        :draggable="true"
+        @dragend="(ev) => addTemplateParam(item, ev)"
+        class="tag-item"
+        type="info"
+      >
+        {{ item.label }}
+      </el-tag>
+      <!-- <el-input
+        v-if="inputVisible"
+        ref="InputRef"
+        v-model="inputValue"
+        class="add-tag-input"
+        size="small"
+        @keyup.enter="handleInputConfirm"
+        @blur="handleInputConfirm"
+      />
+      <el-button v-else class="add-tag-btn" size="small" @click="showInput" icon="Plus">
+        鏂板
+      </el-button> -->
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { InputInstance } from 'element-plus';
+import { useTemplateDetailContext } from '../hooks/context';
+import useSelect from '../hooks/select';
+import { TemplateParam } from '@/fabric-editor/customObject';
+import { Message } from '@bole-core/core';
+
+defineOptions({
+  name: 'systemTemplateDataParamSetting',
+});
+
+const { fabric, canvasEditor } = useSelect();
+
+const { templateParamList, templateEditState } = useTemplateDetailContext();
+
+const state = reactive({
+  isDrawingLineMode: false,
+  lineType: false as boolean | string,
+});
+
+// 榛樿灞炴��
+const defaultPosition = { shadow: '', fontFamily: 'arial' };
+
+const addTemplateParam = (
+  item: API.SelectOptionStringGetDictionaryDataSelectQueryResultOption,
+  event?: DragEvent
+) => {
+  cancelDraw();
+  const templateParam = new TemplateParam(item.label, {
+    ...defaultPosition,
+    fontSize: 24,
+    fill: '#333333',
+    textBackgroundColor: '#ffe084',
+    editable: false,
+
+    templateParamType: EnumContractTemplateValueType.Text,
+    recorder: EnumContractTemplateValueRecorder.Creator,
+    userType: EnumUserType.Personal,
+    required: true,
+    label: item.label,
+    name: item.data?.field3,
+
+    templateDataParamId: '',
+    pageNum: templateEditState.currentImageIndex,
+  });
+
+  console.log('templateParam: ', templateParam);
+  canvasEditor.addBaseType(templateParam, { center: true, event });
+};
+
+const endConflictTools = () => {
+  canvasEditor.discardPolygon();
+  canvasEditor.endDraw();
+  canvasEditor.endTextPathDraw();
+};
+
+const ensureObjectSelEvStatus = (evented: boolean, selectable: boolean) => {
+  canvasEditor.canvas.forEachObject((obj) => {
+    if (obj.id !== 'workspace') {
+      obj.selectable = selectable;
+      obj.evented = evented;
+    }
+  });
+};
+
+const inputValue = ref('');
+const inputVisible = ref(false);
+const InputRef = ref<InputInstance>();
+
+function handleClose(item: API.GetContractTemplateQueryResultValue) {
+  templateParamList.value = templateParamList.value.filter((i) => i.label !== item.label);
+}
+
+const showInput = () => {
+  inputVisible.value = true;
+  nextTick(() => {
+    InputRef.value!.input!.focus();
+  });
+};
+
+const handleInputConfirm = () => {
+  if (inputValue.value) {
+    if (templateParamList.value.some((x) => x.label === inputValue.value)) {
+      Message.warnMessage('宸插瓨鍦ㄧ浉鍚屾爣绛�');
+      return;
+    }
+    // templateParamList.value.push({
+    //   label: inputValue.value,
+    //   type: EnumContractTemplateValueType.Text,
+    //   recorder: EnumContractTemplateValueRecorder.Creator,
+    //   userType: EnumUserType.Personal,
+    //   name: '',
+    //   required: true,
+    //   id: '',
+    // });
+  }
+  inputVisible.value = false;
+  inputValue.value = '';
+};
+
+// 閫�鍑虹粯鍒剁姸鎬�
+const cancelDraw = () => {
+  if (!state.isDrawingLineMode) return;
+  state.isDrawingLineMode = false;
+  state.lineType = '';
+  canvasEditor.setMode(false);
+  endConflictTools();
+  ensureObjectSelEvStatus(true, true);
+};
+
+onDeactivated(() => {
+  cancelDraw();
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+
+.tool-box {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  --bole-pro-form-input-height: 0px;
+
+  .tag-item {
+    cursor: pointer;
+  }
+
+  .add-tag-btn {
+    width: 60px;
+    height: auto !important;
+  }
+
+  .add-tag-input {
+    width: 60px;
+    min-height: 0 !important;
+  }
+
+  /* span {
+    margin-left: 2px;
+    padding: 5px 0;
+    text-align: center;
+    background: #f6f6f6;
+    flex: 1;
+    cursor: pointer;
+
+    &:hover {
+      background: #edf9ff;
+
+      svg {
+        fill: getCssVar('color', 'primary');
+      }
+    }
+  } */
+}
+</style>
diff --git a/src/fabric-editor/components/tools.vue b/src/fabric-editor/components/tools.vue
new file mode 100644
index 0000000..0f6ee55
--- /dev/null
+++ b/src/fabric-editor/components/tools.vue
@@ -0,0 +1,282 @@
+<template>
+  <div>
+    <el-divider content-position="left">鍩虹瑕佺礌</el-divider>
+    <div class="tool-box">
+      <span @click="() => addText()" :draggable="true" @dragend="addText">
+        <textIcon width="26" height="26"></textIcon>
+      </span>
+      <span @click="() => addTextBox()" :draggable="true" @dragend="addTextBox">
+        <textBoxIcon width="26" height="26"></textBoxIcon>
+      </span>
+      <span @click="() => addRect()" :draggable="true" @dragend="addRect">
+        <rectIcon width="26" height="26"></rectIcon>
+      </span>
+      <span @click="() => addCircle()" :draggable="true" @dragend="addCircle">
+        <circleIcon width="26" height="26"></circleIcon>
+      </span>
+      <span @click="() => addTriangle()" :draggable="true" @dragend="addTriangle">
+        <triangleIcon width="26" height="26"></triangleIcon>
+      </span>
+
+      <span @click="() => addPolygon()" :draggable="true" @dragend="addPolygon">
+        <polygonIcon width="26" height="26"></polygonIcon>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup name="Tools">
+import { getPolygonVertices } from '@/fabric-editor/utils/math';
+import useSelect from '@/fabric-editor/hooks/select';
+import circleIcon from '@/fabric-editor/assets/icon/tools/circle.svg?component';
+// import draw1Icon from '@/assets/icon/tools/draw1.svg';
+// import draw2Icon from '@/assets/icon/tools/draw2.svg';
+// import draw3Icon from '@/assets/icon/tools/draw3.svg';
+// import draw4Icon from '@/assets/icon/tools/draw4.svg';
+
+import polygonIcon from '@/fabric-editor/assets/icon/tools/polygon.svg?component';
+import rectIcon from '@/fabric-editor/assets/icon/tools/rect.svg?component';
+import textIcon from '@/fabric-editor/assets/icon/tools/text.svg?component';
+import textBoxIcon from '@/fabric-editor/assets/icon/tools/textBox.svg?component';
+import triangleIcon from '@/fabric-editor/assets/icon/tools/triangle.svg?component';
+
+// import qrCodeIcon from '@/assets/icon/tools/qrCode.svg';
+// import barCodeIcon from '@/assets/icon/tools/barCode.svg';
+
+// import useCalculate from '@/hooks/useCalculate';
+// const { getCanvasBound, isOutsideCanvas } = useCalculate();
+
+const LINE_TYPE = {
+  polygon: 'polygon',
+  freeDraw: 'freeDraw',
+  pathText: 'pathText',
+};
+// 榛樿灞炴��
+const defaultPosition = { shadow: '', fontFamily: 'arial' };
+
+const { fabric, canvasEditor } = useSelect();
+const state = reactive({
+  isDrawingLineMode: false,
+  lineType: false,
+});
+
+const addText = (event) => {
+  cancelDraw();
+  const text = new fabric.IText('涓囦簨澶у悏', {
+    ...defaultPosition,
+    fontSize: 40,
+    fill: '#000000FF',
+  });
+
+  console.log('canvasEditor: ', canvasEditor);
+  canvasEditor.addBaseType(text, { center: true, event });
+};
+
+const addTextBox = (event) => {
+  cancelDraw();
+  const text = new fabric.Textbox('璇镐簨椤洪亗', {
+    ...defaultPosition,
+    splitByGrapheme: true,
+    width: 400,
+    fontSize: 80,
+    fill: '#000000FF',
+  });
+
+  canvasEditor.addBaseType(text, { center: true, event });
+};
+
+const addTriangle = (event) => {
+  cancelDraw();
+  const triangle = new fabric.Triangle({
+    ...defaultPosition,
+    width: 400,
+    height: 400,
+    fill: '#92706BFF',
+    name: '涓夎褰�',
+  });
+  canvasEditor.addBaseType(triangle, { center: true, event });
+};
+
+const addPolygon = (event) => {
+  cancelDraw();
+  const polygon = new fabric.Polygon(getPolygonVertices(5, 200), {
+    ...defaultPosition,
+    fill: '#CCCCCCFF',
+    name: '澶氳竟褰�',
+  });
+  polygon.set({
+    // 鍒涘缓瀹岃缃楂橈紝涓嶇劧瀹介珮浼氬彉鎴愯嚜鍔ㄧ殑鍊�
+    width: 400,
+    height: 400,
+    // 鍏抽棴鍋忕Щ
+    pathOffset: {
+      x: 0,
+      y: 0,
+    },
+  });
+  canvasEditor.addBaseType(polygon, { center: true, event });
+};
+
+const addCircle = (event) => {
+  cancelDraw();
+  const circle = new fabric.Circle({
+    ...defaultPosition,
+    radius: 150,
+    fill: '#57606BFF',
+    // id: uuid(),
+    name: '鍦嗗舰',
+  });
+  canvasEditor.addBaseType(circle, { center: true, event });
+};
+
+const addRect = (event) => {
+  cancelDraw();
+  const rect = new fabric.Rect({
+    ...defaultPosition,
+    fill: '#F57274FF',
+    width: 400,
+    height: 400,
+    name: '鐭╁舰',
+  });
+
+  canvasEditor.addBaseType(rect, { center: true, event });
+};
+const drawPolygon = () => {
+  const onEnd = () => {
+    state.lineType = false;
+    state.isDrawingLineMode = false;
+    ensureObjectSelEvStatus(!state.isDrawingLineMode, !state.isDrawingLineMode);
+  };
+  if (state.lineType !== LINE_TYPE.polygon) {
+    endConflictTools();
+    endDrawingLineMode();
+    state.lineType = LINE_TYPE.polygon;
+    state.isDrawingLineMode = true;
+    canvasEditor.beginDrawPolygon(onEnd);
+    canvasEditor.endDraw();
+    ensureObjectSelEvStatus(!state.isDrawingLineMode, !state.isDrawingLineMode);
+  } else {
+    canvasEditor.discardPolygon();
+  }
+};
+
+const drawPathText = () => {
+  if (state.lineType === LINE_TYPE.pathText) {
+    state.lineType = false;
+    state.isDrawingLineMode = false;
+    canvasEditor.endTextPathDraw();
+  } else {
+    endConflictTools();
+    endDrawingLineMode();
+    state.lineType = LINE_TYPE.pathText;
+    state.isDrawingLineMode = true;
+    canvasEditor.startTextPathDraw();
+  }
+};
+
+const freeDraw = () => {
+  if (state.lineType === LINE_TYPE.freeDraw) {
+    canvasEditor.endDraw();
+    state.lineType = false;
+    state.isDrawingLineMode = false;
+  } else {
+    endConflictTools();
+    endDrawingLineMode();
+    state.lineType = LINE_TYPE.freeDraw;
+    state.isDrawingLineMode = true;
+    canvasEditor.startDraw({ width: 20 });
+  }
+};
+
+const endConflictTools = () => {
+  canvasEditor.discardPolygon();
+  canvasEditor.endDraw();
+  canvasEditor.endTextPathDraw();
+};
+const endDrawingLineMode = () => {
+  state.isDrawingLineMode = false;
+  state.lineType = '';
+  canvasEditor.setMode(state.isDrawingLineMode);
+  canvasEditor.setLineType(state.lineType);
+};
+const drawingLineModeSwitch = (type) => {
+  if ([LINE_TYPE.polygon, LINE_TYPE.freeDraw, LINE_TYPE.pathText].includes(state.lineType)) {
+    endConflictTools();
+  }
+  if (state.lineType === type) {
+    state.isDrawingLineMode = false;
+    state.lineType = '';
+  } else {
+    state.isDrawingLineMode = true;
+    state.lineType = type;
+  }
+  canvasEditor.setMode(state.isDrawingLineMode);
+  canvasEditor.setLineType(type);
+  // this.canvasEditor.setMode(this.isDrawingLineMode);
+  // this.canvasEditor.setArrow(isArrow);
+  ensureObjectSelEvStatus(!state.isDrawingLineMode, !state.isDrawingLineMode);
+};
+
+const ensureObjectSelEvStatus = (evented, selectable) => {
+  canvasEditor.canvas.forEachObject((obj) => {
+    if (obj.id !== 'workspace') {
+      obj.selectable = selectable;
+      obj.evented = evented;
+    }
+  });
+};
+
+// 閫�鍑虹粯鍒剁姸鎬�
+const cancelDraw = () => {
+  if (!state.isDrawingLineMode) return;
+  state.isDrawingLineMode = false;
+  state.lineType = '';
+  canvasEditor.setMode(false);
+  endConflictTools();
+  ensureObjectSelEvStatus(true, true);
+};
+
+onDeactivated(() => {
+  cancelDraw();
+});
+</script>
+
+<style scoped lang="scss">
+@use '@/style/common.scss' as *;
+
+.tool-box {
+  display: flex;
+  justify-content: space-around;
+
+  span {
+    margin-left: 2px;
+    padding: 5px 0;
+    text-align: center;
+    background: #f6f6f6;
+    flex: 1;
+    cursor: pointer;
+
+    &:hover {
+      background: #edf9ff;
+
+      svg {
+        fill: getCssVar('color', 'primary');
+      }
+    }
+  }
+
+  .bg {
+    background: #d8d8d8;
+
+    &:hover {
+      svg {
+        fill: getCssVar('color', 'primary');
+      }
+    }
+  }
+}
+
+.img {
+  width: 20px;
+}
+</style>
diff --git a/src/fabric-editor/components/waterMark.vue b/src/fabric-editor/components/waterMark.vue
new file mode 100644
index 0000000..6c5b3c3
--- /dev/null
+++ b/src/fabric-editor/components/waterMark.vue
@@ -0,0 +1,181 @@
+<!--
+ * @Author: June
+ * @Description:
+ * @Date: 2023-11-01 11:54:10
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-04-22 00:51:43
+-->
+<template>
+  <Button type="text" @click="addWaterMark">
+    {{ $t('waterMark.text') }}
+  </Button>
+
+  <Modal v-model="showWaterMadal" :title="$t('waterMark.modalTitle')">
+    <div class="setting-item required">
+      <span class="mr-10px">{{ $t('waterMark.setting.name') }}</span>
+      <Input
+        class="w-320"
+        v-model="waterMarkState.text"
+        maxlength="15"
+        show-word-limit
+        :placeholder="$t('placeholder')"
+      />
+    </div>
+    <div class="setting-item font-selector">
+      <span class="mr-10px">閫夋嫨瀛椾綋</span>
+      <Select class="w-320" v-model="waterMarkState.fontFamily" @on-change="changeFontFamily">
+        <Option v-for="item in fontsList" :value="item.name" :key="`font-${item.name}`">
+          <div class="font-item" v-if="!item.img">{{ item.name }}</div>
+          <div class="font-item" v-else :style="`background-image:url('${item.img}');`">
+            {{ !item.img ? item : '' }}
+            <!-- 瑙e喅鏃犳硶閫変腑闂 -->
+            <span style="display: none">{{ item.name }}</span>
+          </div>
+        </Option>
+      </Select>
+    </div>
+    <div class="setting-item">
+      <span class="mr-10px">{{ $t('waterMark.setting.size') }}</span>
+
+      <Slider class="w-320" v-model="waterMarkState.size" :min="18" :max="48"></Slider>
+    </div>
+    <div class="setting-item">
+      <span class="mr-10px">{{ $t('waterMark.setting.color') }}</span>
+
+      <ColorPicker v-model="waterMarkState.color" alpha size="small" />
+    </div>
+    <div class="setting-item">
+      <span class="mr-10px">{{ $t('waterMark.setting.position.label') }}</span>
+
+      <RadioGroup v-model="waterMarkState.position">
+        <Radio :label="POSITION.lt">{{ $t('waterMark.setting.position.lt') }}</Radio>
+        <Radio :label="POSITION.rt">{{ $t('waterMark.setting.position.rt') }}</Radio>
+        <Radio :label="POSITION.lb">{{ $t('waterMark.setting.position.lb') }}</Radio>
+        <Radio :label="POSITION.rb">{{ $t('waterMark.setting.position.rb') }}</Radio>
+        <Radio :label="POSITION.full">{{ $t('waterMark.setting.position.full') }}</Radio>
+      </RadioGroup>
+    </div>
+    <div class="setting-item" v-show="waterMarkState.position === POSITION.full">
+      <span class="mr-10px">{{ $t('waterMark.setting.angle') }}</span>
+
+      <div>
+        <RadioGroup v-model="waterMarkState.isRotate">
+          <Radio :label="0">妯悜</Radio>
+          <Radio :label="1">鍊炬枩</Radio>
+        </RadioGroup>
+      </div>
+    </div>
+    <template #footer>
+      <Button type="text" @click="onCleanUpWaterMark">
+        {{ `${$t('cleanUp')}${$t('waterMark.text')}` }}
+      </Button>
+      <Button type="primary" @click="onModalOk">纭畾</Button>
+    </template>
+  </Modal>
+</template>
+
+<script name="WaterMark" lang="ts" setup>
+import { cloneDeep, debounce } from 'lodash-es';
+import useSelect from '@/hooks/select';
+// import { useFont } from '@/hooks';
+import { Message } from 'view-ui-plus';
+enum POSITION {
+  lt = 'Left_Top',
+  lb = 'Left_Right',
+  rt = 'Right_Top',
+  rb = 'Right_Bottom',
+  full = 'Full',
+}
+
+type IPosition = POSITION.lt | POSITION.lb | POSITION.rt | POSITION.rb | POSITION.full; // lt 宸︿笂 lr 宸︿笂 rt 鍙充笂  rb 鍙充笅 full 骞抽摵 鍚庣画鍙墿灞曞叾浠栧姛鑳�
+
+type IDrawOps = {
+  text: string;
+  size: number;
+  fontFamily: string;
+  color: string;
+  isRotate: boolean;
+  position: IPosition;
+};
+const { canvasEditor }: any = useSelect();
+
+const fontsList: any = ref([]);
+canvasEditor.getFontList().then((list: any) => {
+  fontsList.value = list;
+});
+const waterMarkState: any = reactive({
+  text: '',
+  size: 24,
+  isRotate: 0, // 缁勪欢涓嶆敮鎸乥oolean
+  fontFamily: '姹変綋', // 鍙�冭檻鑷畾涔夊瓧浣�
+  color: '#ccc', // 鍙�冭檻鑷畾涔夐鑹�
+  position: POSITION.lt, // lt 宸︿笂 rt 鍙充笂 lb 宸︿笅  rb 鍙充笅 full 骞抽摵
+});
+
+const showWaterMadal = ref(false);
+
+const onCleanUpWaterMark = () => {
+  waterMarkState.text = '';
+  waterMarkState.size = 24;
+  waterMarkState.fontFamily = 'serif';
+  waterMarkState.color = '#ccc';
+  waterMarkState.position = POSITION.lt;
+  waterMarkState.isRotate = 0;
+  canvasEditor.clearWaterMMatk();
+  showWaterMadal.value = false;
+};
+
+const onModalOk = async () => {
+  if (!waterMarkState.text) return Message.warning('姘村嵃鍚嶅瓧涓嶈兘涓虹┖');
+  const ops: IDrawOps = cloneDeep(waterMarkState);
+  ops.isRotate = !!ops.isRotate; // 杞负瀵瑰簲绫诲瀷  鍚庣画鍐嶇粺涓�澶勭悊绫诲瀷
+  await canvasEditor.drawWaterMark(ops);
+  showWaterMadal.value = false;
+  // onMadalCancel();
+};
+
+const changeFontFamily = (fontName: string) => {
+  if (!fontName) return;
+  canvasEditor.loadFont(fontName);
+};
+
+const addWaterMark = debounce(function () {
+  showWaterMadal.value = true;
+}, 250);
+</script>
+
+<style lang="less" scoped>
+.mr-10px {
+  margin-right: 10px;
+}
+.w-320 {
+  width: 320px;
+}
+.setting-item {
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin-bottom: 10px;
+  position: relative;
+  &.required::before {
+    content: '*';
+    color: red;
+    position: absolute;
+    top: 3px;
+    left: -6px;
+  }
+}
+.font-selector {
+  :deep(.ivu-select-item) {
+    padding: 1px 4px;
+  }
+
+  .font-item {
+    height: 40px;
+    width: 330px;
+    background-size: auto 40px;
+    background-repeat: no-repeat;
+  }
+}
+</style>
diff --git a/src/fabric-editor/components/workspaceMask.vue b/src/fabric-editor/components/workspaceMask.vue
new file mode 100644
index 0000000..b0d5601
--- /dev/null
+++ b/src/fabric-editor/components/workspaceMask.vue
@@ -0,0 +1,37 @@
+<template>
+  <div class="mask-wrap">
+    <div>寮�鍚儗鏅挋鐗�</div>
+
+    <iSwitch v-model="openMask" size="large" @on-change="onMaskChange">
+      <template #open>
+        <span>寮�鍚�</span>
+      </template>
+      <template #close>
+        <span>鍏抽棴</span>
+      </template>
+    </iSwitch>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import useSelect from '@/hooks/select';
+
+const { canvasEditor }: any = useSelect();
+
+const openMask = ref(false);
+const onMaskChange = () => {
+  canvasEditor?.workspaceMaskToggle();
+};
+
+onMounted(() => {
+  openMask.value = canvasEditor?.getworkspaceMaskStatus();
+});
+</script>
+
+<style lang="less" scoped>
+.mask-wrap {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>
diff --git a/src/fabric-editor/components/zoom.vue b/src/fabric-editor/components/zoom.vue
new file mode 100644
index 0000000..1ccc7a6
--- /dev/null
+++ b/src/fabric-editor/components/zoom.vue
@@ -0,0 +1,55 @@
+<!--
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2022-04-21 20:20:20
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2023-08-05 18:44:54
+ * @Description: 缂╂斁鍏冪礌
+-->
+<template>
+  <div class="box">
+    <ButtonGroup>
+      <Button @click="big">
+        <bigIcon width="14" height="14"></bigIcon>
+      </Button>
+      <Button @click="small">
+        <smallIcon width="14" height="14"></smallIcon>
+      </Button>
+      <Button @click="rSet" icon="ios-expand"></Button>
+      <Button @click="setViewport" icon="md-contract"></Button>
+    </ButtonGroup>
+  </div>
+</template>
+
+<script setup name="Zoom">
+import useSelect from '@/hooks/select';
+import bigIcon from '@/assets/icon/zoom/big.svg';
+import smallIcon from '@/assets/icon/zoom/small.svg';
+
+const { canvasEditor } = useSelect();
+
+const rSet = () => {
+  canvasEditor.one();
+};
+const big = () => {
+  canvasEditor.big();
+};
+const small = () => {
+  canvasEditor.small();
+};
+const setViewport = () => {
+  canvasEditor.auto();
+};
+</script>
+<style scoped lang="less">
+.box {
+  position: absolute;
+  right: 10px;
+  bottom: 10px;
+
+  :deep(.ivu-btn:hover) {
+    svg {
+      fill: #57a3f3;
+    }
+  }
+}
+</style>
diff --git a/src/fabric-editor/config/constants/app.ts b/src/fabric-editor/config/constants/app.ts
new file mode 100644
index 0000000..3108db3
--- /dev/null
+++ b/src/fabric-editor/config/constants/app.ts
@@ -0,0 +1 @@
+export const LANG = 'lang'; // 澶氳瑷�key
diff --git a/src/fabric-editor/config/constants/filter.ts b/src/fabric-editor/config/constants/filter.ts
new file mode 100644
index 0000000..24f96ee
--- /dev/null
+++ b/src/fabric-editor/config/constants/filter.ts
@@ -0,0 +1,192 @@
+// UI绫诲瀷
+export const uiType = {
+  SELECT: 'select',
+  COLOR: 'color',
+  NUMBER: 'number',
+};
+
+// 鏈夊弬鏁版护闀�
+export const paramsFilters = [
+  {
+    type: 'Brightness',
+    status: false,
+    params: [
+      {
+        key: 'brightness',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Contrast',
+    status: false,
+    params: [
+      {
+        key: 'contrast',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Saturation',
+    status: false,
+    params: [
+      {
+        key: 'saturation',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Vibrance',
+    status: false,
+    params: [
+      {
+        key: 'vibrance',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'HueRotation',
+    status: false,
+    params: [
+      {
+        key: 'rotation',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Noise',
+    status: false,
+    params: [
+      {
+        key: 'noise',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: -1,
+        max: 1000,
+        step: 0.1,
+      },
+    ],
+  },
+  {
+    type: 'Pixelate',
+    status: false,
+    params: [
+      {
+        key: 'blocksize',
+        value: 0.01,
+        uiType: uiType.NUMBER,
+        min: 0.01,
+        max: 100,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Blur',
+    status: false,
+    params: [
+      {
+        key: 'blur',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: 0,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+  {
+    type: 'Grayscale',
+    status: false,
+    params: [
+      {
+        key: 'mode',
+        value: 'average',
+        uiType: uiType.SELECT,
+        list: ['average', 'lightness', 'luminosity'],
+      },
+    ],
+  },
+  {
+    type: 'RemoveColor',
+    status: false,
+    params: [
+      {
+        key: 'color',
+        value: '',
+        uiType: uiType.COLOR,
+      },
+      {
+        key: 'distance',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: 0,
+        max: 1,
+        step: 0.01,
+      },
+    ],
+  },
+];
+
+// 缁勫悎寮忓弬鏁版护闀�
+export const combinationFilters = [
+  {
+    type: 'Gamma',
+    status: false,
+    params: [
+      {
+        key: 'red',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: 0.01,
+        max: 2.2,
+        step: 0.01,
+      },
+      {
+        key: 'green',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: 0.01,
+        max: 2.2,
+        step: 0.01,
+      },
+      {
+        key: 'blue',
+        value: 0,
+        uiType: uiType.NUMBER,
+        min: 0.01,
+        max: 2.2,
+        step: 0.01,
+      },
+    ],
+    handler(red: number | string, green: number | string, blue: number | string) {
+      return {
+        gamma: [red, green, blue],
+      };
+    },
+  },
+];
diff --git a/src/fabric-editor/customObject/index.ts b/src/fabric-editor/customObject/index.ts
new file mode 100644
index 0000000..2c61a8c
--- /dev/null
+++ b/src/fabric-editor/customObject/index.ts
@@ -0,0 +1,55 @@
+import { fabric } from 'fabric';
+
+export const TemplateParamObjectName = 'template-param';
+
+export const TemplateParamExtensionKey = [
+  'templateParamType',
+  'recorder',
+  'userType',
+  'label',
+  'name',
+  'required',
+  'templateDataParamId',
+  'pageNum',
+];
+
+export class TemplateParam extends fabric.IText {
+  type = TemplateParamObjectName;
+
+  constructor(text: string, options: TemplateParamOptions) {
+    super(text, options);
+  }
+
+  toObject(propertiesToInclude) {
+    //@ts-ignore
+    return this.callSuper('toObject', TemplateParamExtensionKey.concat(propertiesToInclude));
+  }
+}
+
+//@ts-ignore
+fabric.TemplateParam = TemplateParam;
+fabric.Object._fromObject[TemplateParamObjectName] = function (text, objectOptions) {
+  console.log('objectOptions: ', objectOptions);
+  return new TemplateParam(text, objectOptions);
+};
+
+export interface TemplateParamOptions extends fabric.ITextOptions {
+  templateDataParamId: string;
+  pageNum?: number;
+
+  templateParamType?: EnumContractTemplateValueType;
+  recorder?: EnumContractTemplateValueRecorder;
+  userType?: EnumUserType;
+  /** 鍙橀噺鍚嶇О */
+  label?: string;
+  /** 鍙橀噺浠g爜 */
+  name?: string;
+  /** 鏄惁蹇呭~ */
+  required?: boolean;
+}
+
+export interface TemplateParamExtraData {
+  dataParamNameFieldName?: string;
+  templateParamFieldName?: string;
+  pageNum?: number;
+}
diff --git a/src/fabric-editor/hooks/context.ts b/src/fabric-editor/hooks/context.ts
new file mode 100644
index 0000000..6406b85
--- /dev/null
+++ b/src/fabric-editor/hooks/context.ts
@@ -0,0 +1,219 @@
+import { useQuery } from '@tanstack/vue-query';
+import * as electronSignServices from '@/services/api/electronSign';
+import { Ref, Reactive } from 'vue';
+import { setOSSLink } from '@/utils';
+import { TemplateParam, TemplateParamExtraData, TemplateParamObjectName } from '../customObject';
+import useSelect from '../hooks/select';
+import { Message } from '@bole-core/core';
+import { fabric } from 'fabric';
+import { v4 as uuid } from 'uuid';
+import {
+  CanvasJson,
+  checkTemplateParamObjectListNotNull,
+  convertJsonMapToTemplateParamObjectList,
+} from '../utils';
+
+export interface TemplateDetailContext {
+  templateDetail: Ref<API.GetContractTemplateQueryResult>;
+  templateParamList: Ref<API.SelectOptionStringGetDictionaryDataSelectQueryResultOption[]>;
+  templateImageList: Ref<TemplateEditDataItem[]>;
+  templateEditState: Reactive<{
+    templateId: string;
+    currentImageIndex: number;
+    jsonMap: Record<number, CanvasJson>;
+  }>;
+}
+
+export const TemplateDetailPContextKey: InjectionKey<TemplateDetailContext> = Symbol(
+  'TemplateDetailPContextKey'
+);
+
+type UseTemplateDetailProvideOptions = {
+  id: MaybeRef<string>;
+};
+
+export function useTemplateDetailProvide({ id }: UseTemplateDetailProvideOptions) {
+  console.log('id: ', id);
+  // const templateParamList = ref<API.GetContractTemplateQueryResultValue[]>([]);
+
+  const { data: templateDetail, isLoading } = useQuery({
+    queryKey: ['electronSignServices/getContractTemplate', id],
+    queryFn: async () => {
+      return await electronSignServices.getContractTemplate(
+        {
+          id: unref(id),
+        },
+        {
+          showLoading: false,
+        }
+      );
+    },
+    enabled: computed(() => !!unref(id)),
+    placeholderData: () => ({} as API.GetContractTemplateQueryResult),
+    onSuccess(data) {
+      const jsonData = JSON.parse(data.templateJsonData);
+      console.log('jsonData22: ', jsonData);
+      if (data.templateJsonData && jsonData) {
+        templateEditState.jsonMap = jsonData;
+      }
+    },
+  });
+
+  const templateImageList = computed(() => {
+    if (templateDetail.value.templateEditData) {
+      let _templateImageList = JSON.parse(
+        templateDetail.value.templateEditData
+      ) as TemplateEditDataItem[];
+      return _templateImageList
+        ? _templateImageList.map((x) => ({
+            ...x,
+            path: setOSSLink(x.path),
+          }))
+        : [];
+    }
+    return [];
+  });
+
+  const { dictionaryDataList: templateParamList } = useDictionaryDataSelect({
+    categoryCode: computed(() => CategoryCode.ElectronSignParam),
+  });
+
+  const templateEditState = reactive({
+    templateId: unref(id),
+    currentImageIndex: 0,
+    jsonMap: {} as Record<number, CanvasJson>,
+  });
+
+  provide(TemplateDetailPContextKey, {
+    templateDetail,
+    templateParamList,
+    templateImageList,
+    templateEditState,
+  });
+
+  return {
+    isLoading,
+  };
+}
+
+export function useTemplateDetailContext() {
+  return inject(TemplateDetailPContextKey);
+}
+
+export function useCanvasActions() {
+  const { canvasEditor } = useSelect();
+  const { templateEditState, templateImageList } = useTemplateDetailContext();
+
+  const clear = () => {
+    canvasEditor.clear();
+    clearHistory();
+  };
+
+  function clearHistory() {
+    // canvasEditor.canvas.clearHistory(false);
+    // canvasEditor.historyUpdate();
+  }
+
+  function toggleCanvas(index: number, force = false) {
+    if (index !== templateEditState.currentImageIndex || force) {
+      let json = canvasEditor.getJson() as CanvasJson;
+      console.log('toggle json: ', json);
+      templateEditState.jsonMap[templateEditState.currentImageIndex] = json;
+      clear();
+      templateEditState.currentImageIndex = index;
+      const jsonData = templateEditState.jsonMap[index];
+      if (jsonData) {
+        canvasEditor.loadJSON(JSON.stringify(jsonData), () => {
+          canvasEditor.canvas.discardActiveObject();
+        });
+        // canvasEditor.canvas.loadFromJSON(JSON.stringify(jsonData), () => {
+        //   // const imgs = canvasEditor.canvas
+        //   //   .getObjects()
+        //   //   .find((o) => o.type === 'image') as fabric.Image[];
+        //   // console.log('imgs: ', imgs);
+        //   // imgs[0].evented = false;
+        //   // imgs[0].selectable = false;
+        //   // imgs.forEach(setImageLock);
+        //   // canvasEditor.canvas.renderAll();
+        // });
+      } else {
+        setBackgroundImage(templateImageList.value[index].path);
+        console.log('templateEditState: ', templateEditState);
+      }
+    }
+  }
+
+  const setBackgroundImage = (imageUrl: string) => {
+    fabric.Image.fromURL(imageUrl, function (img: fabric.Image) {
+      setImageLock(img);
+      console.log('img: ', img);
+      canvasEditor.setSize(img.width, img.height);
+      canvasEditor.canvas.add(img);
+      canvasEditor.canvas.renderAll();
+    });
+  };
+
+  function exportCanvas() {
+    let json = canvasEditor.getJson() as CanvasJson;
+    console.log('json: ', json);
+  }
+
+  function saveAllCanvas() {
+    let json = canvasEditor.getJson() as CanvasJson;
+    templateEditState.jsonMap[templateEditState.currentImageIndex] = json;
+    saveCustomerTemplateParam();
+  }
+
+  async function saveCustomerTemplateParam() {
+    try {
+      const templateParamObjectList = convertJsonMapToTemplateParamObjectList(
+        templateEditState.jsonMap
+      );
+      if (!checkTemplateParamObjectListNotNull(templateParamObjectList)) {
+        return;
+      }
+      let params: API.SaveContractTemplateValuesCommand = {
+        id: templateEditState.templateId,
+        values: templateParamObjectList.map(
+          (x) =>
+            ({
+              id: x.templateDataParamId,
+              label: x.label,
+              name: x.name,
+              required: x.required,
+              type: x.templateParamType,
+              recorder: x.recorder,
+              userType: x.userType,
+            } as API.SaveContractTemplateValuesCommandItem)
+        ),
+        templateJsonData: JSON.stringify(templateEditState.jsonMap),
+      };
+      console.log('params: ', params);
+      let res = await electronSignServices.saveContractTemplateValues(params);
+      if (res) {
+        Message.successMessage('宸蹭繚瀛�');
+      }
+    } catch (error) {}
+  }
+
+  return {
+    clear,
+    toggleCanvas,
+    saveAllCanvas,
+    exportCanvas,
+    setBackgroundImage,
+  };
+}
+
+function setImageLock(img: fabric.Image) {
+  img.set({
+    selectable: false, // 涓嶅彲閫変腑
+    evented: false, // 涓嶅搷搴斾簨浠�
+    lockMovementX: true, // 涓嶅搷搴斾簨浠�
+    lockMovementY: true, // 涓嶅搷搴斾簨浠�
+    lockRotation: true, // 閿佸畾鏃嬭浆
+    lockScalingX: true, // 閿佸畾姘村钩缂╂斁
+    lockScalingY: true, // 閿佸畾鍨傜洿缂╂斁
+    hasControls: false,
+  });
+}
diff --git a/src/fabric-editor/hooks/pageList.js b/src/fabric-editor/hooks/pageList.js
new file mode 100644
index 0000000..94ae828
--- /dev/null
+++ b/src/fabric-editor/hooks/pageList.js
@@ -0,0 +1,210 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-17 11:00:14
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-09 17:02:23
+ * @Description: 鍒嗛〉閫氱敤
+ */
+
+const repoSrc = import.meta.env.APP_APIHOST;
+import axios from 'axios';
+import qs from 'qs';
+
+// 鍒嗙被API
+const typeApi = (url) => axios.get(`${repoSrc}/api/${url}?pagination[pageSize]=200`);
+
+// 鍒嗛〉API
+const pageApi = (url, queryParams) => axios.get(`${repoSrc}/api/${url}?${queryParams}`);
+
+const getInfo = (id) => axios.get(`${repoSrc}/api/templs/${id}`);
+
+function getQueryParams(option, filters) {
+  filters.forEach((item) => {
+    const { key, value, type } = item;
+    if (value) {
+      option.filters[key] = { [type]: value };
+    }
+  });
+  return qs.stringify(option);
+}
+
+function getPageParams(
+  page,
+  typeValue,
+  searchKeyWord,
+  searchTypeKey,
+  searchWordKey,
+  pageSize,
+  fields
+) {
+  const query = {
+    populate: {
+      img: '*',
+    },
+    filters: {},
+    fields,
+    pagination: {
+      page: page,
+      pageSize: pageSize,
+    },
+  };
+
+  const queryParams = getQueryParams(query, [
+    {
+      key: searchTypeKey,
+      value: typeValue,
+      type: '$eq',
+    },
+    {
+      key: searchWordKey,
+      value: searchKeyWord,
+      type: '$contains',
+    },
+  ]);
+  return queryParams;
+}
+
+function getMaterialInfoUrl(info) {
+  const imgUrl = info?.data?.attributes?.url || '';
+  return repoSrc + imgUrl;
+}
+
+function getMaterialPreviewUrl(info) {
+  const imgUrl = info?.data?.attributes?.formats?.small?.url || info?.data?.attributes?.url || '';
+  return repoSrc + imgUrl;
+}
+
+export default function usePageList({
+  typeUrl,
+  listUrl,
+  searchTypeKey,
+  searchWordKey,
+  scrollElement,
+  pageSize,
+  fields = [],
+}) {
+  const pageLoading = ref(false);
+
+  // 鍏抽敭璇�
+  const searchKeyWord = ref('');
+  // 鍒嗙被
+  const typeValue = ref('');
+  const typeList = ref([]);
+  const typeText = computed(() => {
+    const info = typeList.value.find((item) => item.value === typeValue.value);
+    return info?.lable || '鍏ㄩ儴';
+  });
+
+  // 绱犳潗鍒楄〃
+  const pageData = ref([]);
+  const page = ref(1);
+  const pagination = reactive({
+    page: 0,
+    pageCount: 0,
+    pageSize: 10,
+    total: 0,
+  });
+
+  // 鏄惁鍒拌揪搴曢儴
+  const isDownBottm = computed(() => {
+    return pagination.page === page.value && pagination.page >= pagination.pageCount;
+  });
+  // 鑾峰彇鍒嗙被鍒楄〃
+  const getTypeList = async () => {
+    pageLoading.value = true;
+    try {
+      const res = await typeApi(typeUrl);
+      const list = res.data.data.map((item) => {
+        return {
+          value: item.id,
+          label: item.attributes.name,
+        };
+      });
+      typeList.value = [
+        {
+          label: '鍏ㄩ儴',
+          value: '',
+        },
+        ...list,
+      ];
+    } catch (error) {
+      typeList.value = [];
+    }
+    pageLoading.value = false;
+  };
+
+  const getPageData = async () => {
+    pageLoading.value = true;
+    try {
+      const params = getPageParams(
+        page.value,
+        typeValue.value,
+        searchKeyWord.value,
+        searchTypeKey,
+        searchWordKey,
+        pageSize,
+        fields
+      );
+      const res = await pageApi(listUrl, params);
+      const list = res.data.data.map((item) => {
+        return {
+          id: item.id,
+          name: item.attributes.name,
+          desc: item.attributes.desc,
+          json: item.attributes?.json,
+          src: getMaterialInfoUrl(item.attributes.img),
+          previewSrc: getMaterialPreviewUrl(item.attributes.img),
+        };
+      });
+      Object.keys(res.data.meta.pagination).forEach((key) => {
+        pagination[key] = res.data.meta.pagination[key];
+      });
+      pageData.value = [...pageData.value, ...list];
+    } catch (error) {
+      console.log(error);
+    }
+    pageLoading.value = false;
+  };
+
+  const startGetList = () => {
+    pageData.value = [];
+    page.value = 1;
+    getPageData();
+  };
+
+  const nextPage = () => {
+    if (page.value >= pagination.pageCount) return;
+    page.value++;
+    setTimeout(() => {
+      getPageData();
+    }, 1000);
+  };
+
+  const showScroll = ref(false);
+  const scrollHeight = ref(0);
+  const startPage = async () => {
+    // 婊氬姩
+    const myTemplBox = document.querySelector(scrollElement);
+    scrollHeight.value = myTemplBox.offsetHeight;
+    showScroll.value = true;
+
+    await getTypeList();
+    await getPageData();
+  };
+
+  return {
+    startPage,
+    searchKeyWord,
+    typeValue,
+    typeText,
+    typeList,
+    pageLoading,
+    pageData,
+    isDownBottm,
+    startGetList,
+    nextPage,
+    scrollHeight,
+    showScroll,
+    getInfo,
+  };
+}
diff --git a/src/fabric-editor/hooks/select.ts b/src/fabric-editor/hooks/select.ts
new file mode 100644
index 0000000..db282b9
--- /dev/null
+++ b/src/fabric-editor/hooks/select.ts
@@ -0,0 +1,96 @@
+import { inject, computed, reactive, onMounted, onBeforeMount } from 'vue';
+import { fabric } from 'fabric';
+
+import Editor, { EventType } from '@muaitu/fabric-editor-core';
+const { SelectMode, SelectEvent } = EventType;
+
+export default function useSelect(matchType?: Array<string>) {
+  const fabric = inject('fabric') as typeof fabric;
+  const canvasEditor = inject('canvasEditor') as Editor;
+
+  const state = reactive({
+    mSelectMode: SelectMode.EMPTY,
+    mSelectOneType: '',
+    mSelectId: '' as any, // 閫夋嫨id
+    mSelectIds: [] as any, // 閫夋嫨id
+    mSelectActive: [] as fabric.Object[],
+  });
+  const selectOne = (arr: fabric.Object[]) => {
+    state.mSelectMode = SelectMode.ONE;
+    const [item] = arr;
+    if (item) {
+      state.mSelectActive = [item];
+      //@ts-ignore
+      state.mSelectId = item.id;
+      state.mSelectOneType = item.type;
+      //@ts-ignore
+      state.mSelectIds = [item.id];
+    }
+    callBack();
+  };
+
+  const selectMulti = (arr: fabric.Object[]) => {
+    state.mSelectMode = SelectMode.MULTI;
+    state.mSelectId = '';
+    //@ts-ignore
+    state.mSelectIds = arr.map((item) => item.id);
+    callBack();
+  };
+
+  const selectCancel = () => {
+    state.mSelectId = '';
+    state.mSelectIds = [];
+    state.mSelectMode = SelectMode.EMPTY;
+    state.mSelectOneType = '';
+    callBack();
+  };
+
+  let callBack = () => {
+    //
+  };
+  const getObjectAttr = (cb: any) => {
+    callBack = cb;
+  };
+  onMounted(() => {
+    canvasEditor.on(SelectEvent.ONE, selectOne);
+    canvasEditor.on(SelectEvent.MULTI, selectMulti);
+    canvasEditor.on(SelectEvent.CANCEL, selectCancel);
+  });
+
+  onBeforeMount(() => {
+    canvasEditor.off(SelectEvent.ONE, selectOne);
+    canvasEditor.off(SelectEvent.MULTI, selectMulti);
+    canvasEditor.off(SelectEvent.CANCEL, selectCancel);
+  });
+
+  let isMatchType = computed(() => {
+    if (matchType) {
+      return matchType.includes(state.mSelectOneType);
+    }
+    return false;
+  });
+
+  const isOne = computed(() => state.mSelectMode === 'one');
+  const isMultiple = computed(() => state.mSelectMode === 'multiple');
+  const isGroup = computed(() => state.mSelectMode === 'one' && state.mSelectOneType === 'group');
+  const isSelect = computed(() => state.mSelectMode);
+
+  const selectType = computed(() => state.mSelectOneType);
+
+  const matchTypeHander = (types: string[]) => {
+    return computed(() => types.includes(state.mSelectOneType));
+  };
+  return {
+    fabric,
+    canvasEditor,
+    mixinState: state,
+    selectType,
+    isSelect,
+    isGroup,
+    isOne,
+    isMultiple,
+    isMatchType,
+    matchTypeHander,
+    getObjectAttr,
+  };
+}
diff --git a/src/fabric-editor/hooks/useAdmin.js b/src/fabric-editor/hooks/useAdmin.js
new file mode 100644
index 0000000..fa34332
--- /dev/null
+++ b/src/fabric-editor/hooks/useAdmin.js
@@ -0,0 +1,92 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-06-09 13:02:18
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-10 20:21:41
+ * @Description: 绠$悊闈㈡澘绠$悊
+ */
+
+import { updataTempl, uploadImg, deleteImg, getTempl, createdTempl } from '@/api/admin';
+import { Spin } from 'view-ui-plus';
+
+import { useRouter } from 'vue-router';
+
+export default function useMaterial() {
+  const canvasEditor = inject('canvasEditor');
+  const router = useRouter();
+  // 鐢诲竷杞浘鐗�
+  const uploadFileToInfo = async () => {
+    const dataURLtoFile = (dataurl, filename) => {
+      var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
+      }
+      return new File([u8arr], filename, { type: mime });
+    };
+
+    const upload = (base64) => {
+      const file = dataURLtoFile(base64, '123.png');
+      const formData = new FormData();
+      const time = new Date();
+      formData.append('files', file, `${time.getTime()}`);
+      return uploadImg(formData)
+        .then((res) => {
+          const [info] = res.data;
+          return info;
+        })
+        .catch((err) => {
+          console.log(err);
+        });
+    };
+    const base64 = await canvasEditor.preview();
+    // 涓婁紶鍥剧墖
+    const fileInfo = await upload(base64);
+    return fileInfo.id;
+  };
+
+  // 鏇存柊璇︽儏
+  const updataTemplHander = async (id) => {
+    Spin.show();
+    try {
+      const { data: info } = await getTempl(id);
+      const newImgId = await uploadFileToInfo();
+      const json = canvasEditor.getJson();
+      await updataTempl(info.id, {
+        ...info,
+        img: newImgId,
+        json,
+      });
+      deleteImg(info.img.id);
+    } catch (error) {
+      console.log(error);
+    }
+
+    Spin.hide();
+  };
+
+  const createdTemplHander = async (name) => {
+    Spin.show();
+    try {
+      const newImgId = await uploadFileToInfo();
+      const json = canvasEditor.getJson();
+      const res = await createdTempl({
+        name,
+        img: newImgId,
+        json,
+      });
+      router.replace('/?tempId=' + res.data.id + '&admin=true');
+    } catch (error) {
+      console.log(error);
+    }
+
+    Spin.hide();
+  };
+  return {
+    updataTemplHander,
+    createdTemplHander,
+  };
+}
diff --git a/src/fabric-editor/hooks/useCalculate.js b/src/fabric-editor/hooks/useCalculate.js
new file mode 100644
index 0000000..10e7952
--- /dev/null
+++ b/src/fabric-editor/hooks/useCalculate.js
@@ -0,0 +1,26 @@
+/*
+ * @Descripttion: useCalculate
+ * @version:
+ * @Author: wuchenguang1998
+ * @Date: 2024-05-18 15:42:17
+ * @LastEditors: wuchenguang1998
+ * @LastEditTime: 2024-05-18 17:28:34
+ */
+
+export default function useCalculate() {
+  const canvasEditor = inject('canvasEditor');
+
+  // 鑾峰彇鐢诲竷鐨凞OMRect瀵硅薄
+  const getCanvasBound = () => canvasEditor.canvas.getSelectionElement().getBoundingClientRect();
+
+  // 鍒ゆ柇鎷栨嫿缁撴潫鐨勫潗鏍囨槸鍚﹀湪鐢诲竷澶�
+  const isOutsideCanvas = (x, y) => {
+    const { left, right, top, bottom } = getCanvasBound();
+    return x < left || x > right || y < top || y > bottom;
+  };
+
+  return {
+    getCanvasBound,
+    isOutsideCanvas,
+  };
+}
diff --git a/src/fabric-editor/hooks/useFileType.js b/src/fabric-editor/hooks/useFileType.js
new file mode 100644
index 0000000..ce2ce45
--- /dev/null
+++ b/src/fabric-editor/hooks/useFileType.js
@@ -0,0 +1,40 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-29 15:13:58
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-29 15:32:49
+ * @Description: 鏂囦欢澶瑰垎椤�
+ */
+
+import {
+  createFileType as createFileTypeApi,
+  getFileTypeList as getFileTypeListApi,
+} from '@/api/user';
+import { Spin } from 'view-ui-plus';
+export default function useFileType() {
+  const createFileType = async (fileTypeName, parentId = '') => {
+    Spin.show();
+    const res = await createFileTypeApi({
+      data: {
+        name: fileTypeName,
+        parentId,
+      },
+    });
+    Spin.hide();
+    return res;
+  };
+
+  const getFileTypeList = async (params) => {
+    Spin.show();
+    const res = await getFileTypeListApi({
+      data: params,
+    });
+    Spin.hide();
+    return res;
+  };
+
+  return {
+    createFileType,
+    getFileTypeList,
+  };
+}
diff --git a/src/fabric-editor/hooks/useMaterial.js b/src/fabric-editor/hooks/useMaterial.js
new file mode 100644
index 0000000..bb4261f
--- /dev/null
+++ b/src/fabric-editor/hooks/useMaterial.js
@@ -0,0 +1,175 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-11 11:51:59
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-05-30 15:09:18
+ * @Description: 绱犳潗鐩稿叧
+ */
+
+import dayjs from 'dayjs';
+import { useRouter, useRoute } from 'vue-router';
+import { uploadImg, createdTempl, getTmplInfo, updataTempl, removeTempl } from '@/api/user';
+import { Modal } from 'view-ui-plus';
+
+import { useI18n } from 'vue-i18n';
+
+export default function useMaterial() {
+  const { t } = useI18n();
+  const router = useRouter();
+  const route = useRoute();
+  const canvasEditor = inject('canvasEditor');
+
+  // 鍒涘缓妯℃澘
+  const createTmpl = async (width, height, parentId = '') => {
+    canvasEditor.clear();
+    canvasEditor.setSize(width, height);
+    const name = dayjs().format('YYYY[骞碷MM[鏈圿DD[鏃HH[灏忔椂]mm[鍒嗛挓]ss[绉抅') + '鍒涘缓鐨勪綔鍝�';
+    const data = await getCanvasCommonData();
+    // 涓婁紶鍥剧墖
+    const templInfo = await createdTempl({
+      data: {
+        ...data,
+        type: 'file',
+        parentId: String(parentId),
+        name,
+      },
+    });
+    routerToId(templInfo.data.data.id);
+    return templInfo;
+  };
+
+  const createdFileType = async (name, parentId = '') => {
+    await createdTempl({
+      data: {
+        name,
+        type: 'fileType',
+        parentId: String(parentId),
+      },
+    });
+  };
+
+  const createTmplByCommon = async () => {
+    const name = dayjs().format('YYYY[骞碷MM[鏈圿DD[鏃HH[灏忔椂]mm[鍒嗛挓]ss[绉抅') + '鍒涘缓鐨勪綔鍝�';
+    const data = await getCanvasCommonData();
+    // 涓婁紶鍥剧墖
+    const templInfo = await createdTempl({
+      data: {
+        ...data,
+        type: 'file',
+        parentId: '',
+        externalId: route.query?.projectid || null,
+        name,
+      },
+    });
+    return templInfo;
+  };
+
+  // 鑾峰彇鐢诲竷鏁版嵁
+  const getCanvasCommonData = async () => {
+    const json = canvasEditor.getJson();
+    const fileInfo = await uploadFileToInfo();
+    return {
+      json,
+      img: fileInfo.id,
+      desc: '',
+    };
+  };
+
+  // 鐢诲竷杞浘鐗�
+  const uploadFileToInfo = async () => {
+    const dataURLtoFile = (dataurl, filename) => {
+      var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
+      }
+      return new File([u8arr], filename, { type: mime });
+    };
+
+    const upload = (base64) => {
+      const file = dataURLtoFile(base64, '123.png');
+      const formData = new FormData();
+      const time = new Date();
+      formData.append('files', file, `${time.getTime()}`);
+      return uploadImg(formData)
+        .then((res) => {
+          const [info] = res.data;
+          return info;
+        })
+        .catch((err) => {
+          console.log(err);
+        });
+    };
+    const base64 = await canvasEditor.preview();
+    // 涓婁紶鍥剧墖
+    const fileInfo = await upload(base64);
+    return fileInfo;
+  };
+
+  // 鏇存柊璺敱
+  const routerToId = (id) => {
+    router.replace('/?id=' + id);
+  };
+
+  // 鑾峰彇璇︽儏
+  const getTemplInfo = async (id) => {
+    const res = await getTmplInfo(id);
+    return res.data;
+  };
+
+  // 鏇存柊璇︽儏
+  const updataTemplInfo = async (id, name) => {
+    const data = await getCanvasCommonData();
+    name && (data.name = name);
+    await updataTempl(id, {
+      data,
+    });
+  };
+
+  const removeTemplInfo = (id) => {
+    return new Promise((resolve, reject) => {
+      Modal.confirm({
+        title: t('my_spase.remove_templ'),
+        content: `<p>${t('my_spase.remove_templTip')}</p>`,
+        onOk: () => {
+          removeTempl(id).then(resolve).catch(reject);
+        },
+      });
+    });
+  };
+
+  const removeFileType = (id) => {
+    return new Promise((resolve, reject) => {
+      Modal.confirm({
+        title: t('my_spase.remove_file_type'),
+        content: `<p>${t('my_spase.remove_file_type_Tip')}</p>`,
+        onOk: () => {
+          removeTempl(id).then(resolve).catch(reject);
+        },
+      });
+    });
+  };
+
+  const reNameFileType = async (name, id) => {
+    await updataTempl(id, {
+      data: {
+        name,
+      },
+    });
+  };
+
+  return {
+    createTmpl,
+    createTmplByCommon,
+    getTemplInfo,
+    updataTemplInfo,
+    removeTemplInfo,
+    routerToId,
+    createdFileType, // 鍒涘缓鏂囦欢澶�
+    reNameFileType, // 淇敼鏂囦欢澶瑰悕绉�
+    removeFileType, // 鍒犻櫎鏂囦欢澶�
+  };
+}
diff --git a/src/fabric-editor/hooks/usePageList.js b/src/fabric-editor/hooks/usePageList.js
new file mode 100644
index 0000000..6a38a06
--- /dev/null
+++ b/src/fabric-editor/hooks/usePageList.js
@@ -0,0 +1,138 @@
+/*
+ * @Author: 绉﹀皯鍗�
+ * @Date: 2024-05-29 15:39:20
+ * @LastEditors: 绉﹀皯鍗�
+ * @LastEditTime: 2024-06-09 16:52:59
+ * @Description: 閫氱敤鍒嗛〉
+ */
+
+import qs from 'qs';
+import { ref } from 'vue';
+
+const APIHOST = import.meta.env.APP_APIHOST;
+export default function usePageList({
+  el,
+  apiClient,
+  filters = {},
+  sort = [],
+  formatData,
+  fields = [],
+}) {
+  //  婊氬姩鏉℃牴鎹〉闈㈤�傚簲
+  const showScroll = ref(false);
+  const scrollHeight = ref(0);
+  const startPage = async () => {
+    // 婊氬姩
+    const myTemplBox = document.querySelector(el);
+    scrollHeight.value = myTemplBox.offsetHeight;
+    showScroll.value = true;
+
+    await startGetList();
+  };
+
+  // 绱犳潗鍒楄〃
+  const pageData = ref([]);
+  const page = ref(1);
+  const pagination = reactive({
+    page: 0,
+    pageCount: 0,
+    pageSize: 10,
+    total: 0,
+  });
+
+  // 鏄惁鍒拌揪搴曢儴
+  const isDownBottom = computed(() => {
+    return pagination.page === page.value && pagination.page >= pagination.pageCount;
+  });
+
+  const pageLoading = ref(false);
+  const getPageData = async () => {
+    pageLoading.value = true;
+    try {
+      const query = {
+        populate: {
+          img: '*',
+        },
+        filters: {},
+        sort: sort,
+        fields,
+        pagination: {
+          page: page.value,
+          pageSize: page.value.pageSize,
+        },
+      };
+      const params = addFilterParams(query, filters);
+      const res = await apiClient(qs.stringify(params));
+      const list = formatData ? formatData(res.data.data) : res.data.data;
+      Object.keys(res.data.meta.pagination).forEach((key) => {
+        pagination[key] = res.data.meta.pagination[key];
+      });
+      pageData.value = [...pageData.value, ...list];
+    } catch (error) {
+      console.log(error);
+    }
+    // Spin.hide();
+    pageLoading.value = false;
+  };
+
+  const startGetList = () => {
+    pageData.value = [];
+    page.value = 1;
+    getPageData();
+  };
+
+  const nextPage = () => {
+    if (page.value >= pagination.pageCount) return;
+    page.value++;
+    setTimeout(() => {
+      getPageData();
+    }, 1000);
+  };
+
+  const addFilterParams = (query, filters) => {
+    Object.keys(filters).forEach((key) => {
+      const itemFilter = {};
+      Object.keys(filters[key]).forEach((myKey) => {
+        const skip = ['$eq', '$contains'];
+        const isNone = !filters[key][myKey];
+        const isSkip = skip.includes(myKey) && isNone;
+        // 涓嶅ソ鍖呭惈璺宠繃鏉′欢
+        if (!isSkip) {
+          itemFilter[myKey] = filters[key][myKey];
+        } else {
+          // 璺宠繃鏉′欢涓� 鍒ゆ柇鏄惁杩囨护 榛樿杩囨护
+          const isFilterEmpty = filters[key].filterEmpty;
+          if (!isFilterEmpty) {
+            itemFilter[myKey] = filters[key][myKey];
+          }
+        }
+      });
+      query.filters[key] = itemFilter;
+    });
+    return query;
+  };
+
+  return {
+    pageData, // 鍒嗛〉鏁版嵁
+    showScroll,
+    scrollHeight,
+    pageLoading,
+    isDownBottom, // 鏄惁鍒拌揪搴曢儴
+    startPage, // 寮�濮嬪垎椤�
+    getPageData, // 鑾峰彇鍒嗛〉鏁版嵁
+    startGetList, // 浠庣涓�涓紑濮�
+    nextPage, // 涓嬩竴椤�
+  };
+}
+
+const getMaterialInfoUrl = (info) => {
+  const imgUrl = info?.data?.attributes?.url || '';
+  return APIHOST + imgUrl;
+};
+
+const getMaterialPreviewUrl = (info) => {
+  const imgUrl = info?.data?.attributes?.formats?.small?.url || info?.data?.attributes?.url || '';
+  return APIHOST + imgUrl;
+};
+
+export { getMaterialInfoUrl, getMaterialPreviewUrl };
diff --git a/src/fabric-editor/hooks/useSelectListen.ts b/src/fabric-editor/hooks/useSelectListen.ts
new file mode 100644
index 0000000..d410e7e
--- /dev/null
+++ b/src/fabric-editor/hooks/useSelectListen.ts
@@ -0,0 +1,68 @@
+import Editor, { EventType } from '@muaitu/fabric-editor-core';
+import { get } from 'lodash';
+
+const { SelectEvent, SelectMode } = EventType;
+
+export interface Selector {
+  mSelectMode: typeof SelectMode[keyof typeof SelectMode];
+  mSelectOneType: string | undefined;
+  mSelectId: string | undefined;
+  mSelectIds: (string | undefined)[];
+  mSelectActive: unknown[];
+}
+
+export default function useSelectListen(canvasEditor: Editor) {
+  const state = reactive<Selector>({
+    mSelectMode: SelectMode.EMPTY,
+    mSelectOneType: '',
+    mSelectId: '', // 閫夋嫨id
+    mSelectIds: [], // 閫夋嫨id
+    mSelectActive: [],
+  });
+
+  const selectOne = (e: [fabric.Object]) => {
+    state.mSelectMode = SelectMode.ONE;
+    state.mSelectActive = e;
+    if (e[0] && get(e[0], 'clip')) {
+      selectCancel();
+      // state.mSelectId = get(e[0], 'targetId');
+      // state.mSelectOneType = get(e[0], 'targetType');
+      // state.mSelectIds = e.map((item) => get(item, 'targetId'));
+      return;
+    }
+    if (e[0]) {
+      state.mSelectId = e[0].id;
+      state.mSelectOneType = e[0].type;
+      state.mSelectIds = e.map((item) => item.id);
+    }
+  };
+
+  const selectMulti = (e: fabric.Object[]) => {
+    state.mSelectMode = SelectMode.MULTI;
+    state.mSelectId = '';
+    state.mSelectIds = e.map((item) => item.id);
+  };
+
+  const selectCancel = () => {
+    state.mSelectId = '';
+    state.mSelectIds = [];
+    state.mSelectMode = SelectMode.EMPTY;
+    state.mSelectOneType = '';
+  };
+
+  onMounted(() => {
+    canvasEditor.on(SelectEvent.ONE, selectOne);
+    canvasEditor.on(SelectEvent.MULTI, selectMulti);
+    canvasEditor.on(SelectEvent.CANCEL, selectCancel);
+  });
+
+  onBeforeMount(() => {
+    canvasEditor.off(SelectEvent.ONE, selectOne);
+    canvasEditor.off(SelectEvent.MULTI, selectMulti);
+    canvasEditor.off(SelectEvent.CANCEL, selectCancel);
+  });
+
+  return {
+    mixinState: state,
+  };
+}
diff --git a/src/fabric-editor/index.vue b/src/fabric-editor/index.vue
new file mode 100644
index 0000000..e0b3d49
--- /dev/null
+++ b/src/fabric-editor/index.vue
@@ -0,0 +1,266 @@
+<template>
+  <div class="home">
+    <el-container direction="vertical">
+      <!-- 澶撮儴鍖哄煙 -->
+      <Top v-if="state.show" :ruler="state.ruler" @update:ruler="rulerSwitch"></Top>
+      <el-main style="position: relative; display: flex; padding: 0; height: calc(100vh - 64px)">
+        <!-- 宸︿晶鍖哄煙 -->
+        <Left v-if="state.show"></Left>
+        <!-- 鐢诲竷鍖哄煙 -->
+        <div id="workspace">
+          <div class="canvas-box">
+            <div class="inside-shadow"></div>
+            <canvas id="canvas" :class="state.ruler ? 'design-stage-grid' : ''"></canvas>
+            <!-- <dragMode v-if="state.show"></dragMode>
+            <zoom></zoom> -->
+          </div>
+        </div>
+        <Right v-if="state.show"></Right>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<script name="Home" setup lang="ts">
+import Top from './viewcomponents/top/index.vue';
+import Left from './viewcomponents/left/index.vue';
+import Right from './viewcomponents/right/index.vue';
+
+// import zoom from '@/components/zoom.vue';
+// import dragMode from '@/components/dragMode.vue';
+// 鍔熻兘缁勪欢
+import { fabric } from 'fabric';
+
+import Editor, {
+  IEditor,
+  DringPlugin,
+  AlignGuidLinePlugin,
+  ControlsPlugin,
+  // ControlsRotatePlugin,
+  CenterAlignPlugin,
+  LayerPlugin,
+  CopyPlugin,
+  MoveHotKeyPlugin,
+  DeleteHotKeyPlugin,
+  GroupPlugin,
+  DrawLinePlugin,
+  GroupTextEditorPlugin,
+  GroupAlignPlugin,
+  WorkspacePlugin,
+  // HistoryPlugin,
+  FlipPlugin,
+  RulerPlugin,
+  MaterialPlugin,
+  WaterMarkPlugin,
+  FontPlugin,
+  PolygonModifyPlugin,
+  DrawPolygonPlugin,
+  FreeDrawPlugin,
+  PathTextPlugin,
+  PsdPlugin,
+  SimpleClipImagePlugin,
+  BarCodePlugin,
+  QrCodePlugin,
+  ImageStroke,
+  ResizePlugin,
+  LockPlugin,
+  AddBaseTypePlugin,
+  MaskPlugin,
+} from '@muaitu/fabric-editor-core';
+import { useTemplateDetailProvide } from './hooks/context';
+import './styles/contextMenu.scss';
+import './plugin/fabric-history';
+import { MyHistoryPlugin } from './plugin/MyHistoryPlugin';
+import { TemplateParamExtensionKey } from './customObject';
+
+const route = useRoute();
+const id = (route.params.id as string) ?? '';
+
+useTemplateDetailProvide({ id });
+// const { isLoading } = useQuery({
+//   queryKey: ['lgGigWorkerServices/getCustomerTemplateDetail', id],
+//   queryFn: async () => {
+//     return await lgGigWorkerServices.getCustomerTemplateDetail({
+//       id: id,
+//     });
+//   },
+//   enabled: !!id,
+//   placeholderData: () => ({} as API.GetCustomerTemplateDetailOutput),
+// })
+
+const APIHOST = 'https://www.kuaitu.cc';
+
+// 鍒涘缓缂栬緫鍣�
+const canvasEditor = new Editor() as IEditor;
+
+canvasEditor.setMaxListeners(Infinity);
+
+const state = reactive({
+  show: false,
+  select: null,
+  ruler: true,
+});
+
+onMounted(() => {
+  // 鍒濆鍖杅abric
+  const canvas = new fabric.Canvas('canvas', {
+    fireRightClick: true, // 鍚敤鍙抽敭锛宐utton鐨勬暟瀛椾负3
+    stopContextMenu: true, // 绂佹榛樿鍙抽敭鑿滃崟
+    controlsAboveOverlay: true, // 瓒呭嚭clipPath鍚庝粛鐒跺睍绀烘帶鍒舵潯
+    // imageSmoothingEnabled: false, // 瑙e喅鏂囧瓧瀵煎嚭鍚庝笉娓呮櫚闂
+    preserveObjectStacking: true, // 褰撻�夋嫨鐢诲竷涓殑瀵硅薄鏃讹紝璁╁璞′笉鍦ㄩ《灞傘��
+    width: 595,
+    height: 842,
+  });
+  console.log('canvas: ', canvas);
+
+  // 鍒濆鍖栫紪杈戝櫒
+  canvasEditor.init(canvas, TemplateParamExtensionKey);
+  canvasEditor
+    .use(DringPlugin)
+    .use(PolygonModifyPlugin)
+    .use(AlignGuidLinePlugin)
+    .use(ControlsPlugin)
+    // .use(ControlsRotatePlugin)
+    .use(CenterAlignPlugin)
+    // .use(LayerPlugin)
+    .use(CopyPlugin)
+    .use(MoveHotKeyPlugin)
+    .use(DeleteHotKeyPlugin)
+    .use(GroupPlugin)
+    .use(DrawLinePlugin)
+    .use(GroupTextEditorPlugin)
+    .use(GroupAlignPlugin)
+    .use(WorkspacePlugin)
+    .use(MyHistoryPlugin)
+    .use(FlipPlugin)
+    .use(RulerPlugin)
+    .use(DrawPolygonPlugin)
+    .use(FreeDrawPlugin)
+    .use(PathTextPlugin)
+    .use(SimpleClipImagePlugin)
+    .use(BarCodePlugin)
+    .use(QrCodePlugin)
+    .use(FontPlugin, {
+      repoSrc: APIHOST,
+    })
+    .use(MaterialPlugin, {
+      repoSrc: APIHOST,
+    })
+    .use(WaterMarkPlugin)
+    .use(PsdPlugin)
+    .use(ImageStroke)
+    .use(ResizePlugin)
+    .use(LockPlugin)
+    .use(AddBaseTypePlugin)
+    .use(MaskPlugin);
+
+  state.show = true;
+  // 榛樿鎵撳紑鏍囧昂
+  if (state.ruler) {
+    canvasEditor.rulerEnable();
+  }
+});
+
+onUnmounted(() => canvasEditor.destory());
+const rulerSwitch = (val: any) => {
+  state.ruler = val;
+  if (val) {
+    canvasEditor.rulerEnable();
+  } else {
+    canvasEditor.rulerDisable();
+  }
+  // 浣挎爣灏哄紑鍏崇粍浠跺け鐒︼紝閬垮厤鍝嶅簲閿洏鐨勭┖鏍间簨浠�
+  //@ts-ignore
+  document.activeElement.blur();
+};
+
+provide('fabric', fabric);
+provide('canvasEditor', canvasEditor);
+// provide('mixinState', mixinState);
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-header) {
+  --height: 45px;
+  display: flex;
+  justify-content: space-between;
+  padding: 0 0px;
+  height: var(--height);
+  border-bottom: 1px solid #eef2f8;
+  background: #ffffff;
+  line-height: var(--height);
+}
+
+.home,
+.ivu-layout {
+  height: 100%;
+}
+
+.home {
+  :deep() {
+    .el-container {
+      height: 100%;
+    }
+  }
+}
+
+.icon {
+  display: block;
+}
+
+.canvas-box {
+  position: relative;
+}
+
+// 鐢诲竷鍐呴槾褰�
+
+.inside-shadow {
+  position: absolute;
+  z-index: 2;
+  width: 100%;
+  height: 100%;
+  box-shadow: inset 0 0 9px 2px #0000001f;
+  pointer-events: none;
+}
+
+#canvas {
+  margin: 0 auto;
+  width: 300px;
+  height: 300px;
+}
+
+#workspace {
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  background: #f1f1f1;
+  flex: 1;
+}
+
+// 鏍囧昂
+
+.switch {
+  margin-right: 10px;
+}
+
+// 缃戞牸鑳屾櫙
+
+.design-stage-grid {
+  --offsetX: 0px;
+  --offsetY: 0px;
+  --size: 16px;
+  --color: #dedcdc;
+  background-image: linear-gradient(
+      45deg,
+      var(--color) 25%,
+      transparent 0,
+      transparent 75%,
+      var(--color) 0
+    ),
+    linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0);
+  background-position: var(--offsetX) var(--offsetY),
+    calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
+  background-size: calc(var(--size) * 2) calc(var(--size) * 2);
+}
+</style>
diff --git a/src/fabric-editor/language/en.json b/src/fabric-editor/language/en.json
new file mode 100644
index 0000000..f143b02
--- /dev/null
+++ b/src/fabric-editor/language/en.json
@@ -0,0 +1,195 @@
+{
+  "templates": "Templates",
+  "elements": "Elements",
+  "background": "Background",
+  "placeholder": "Please input",
+  "size": "Size",
+  "width": "Width",
+  "height": "Height",
+  "grid": "Grid",
+  "common_elements": "Common Elements",
+  "draw_elements": "Draw Elements",
+  "code_img": "Code Img",
+  "color_macthing": "Macthing",
+  "background_texture": "Background Texture",
+  "picture": "picture",
+  "everything_is_fine": "Everything is fine",
+  "everything_goes_well": "Everything goes well",
+  "cartoon": "Cartoon",
+  "default_size": "Default Size",
+  "preview": "Preview",
+  "empty": "Empty",
+  "keep": "Save",
+  "copy_to_clipboard": "Copy to clipboard",
+  "save_as_picture": "Save as picture",
+  "save_as_svg": "Save as SVG",
+  "save_as_json": "Save as JSON",
+  "layers": "Layers",
+  "title_template": "Title Template",
+  "insert_svg": "Insert SVG",
+  "select_svg": "Select SVG",
+  "please_choose": "Please choose",
+  "string": "string",
+  "file": "file",
+  "import_files": "Import files",
+  "select_json": "Select JSON",
+  "repleaceImg": "repleace Image",
+  "waterMark": {
+    "text": "WaterMark",
+    "modalTitle": "WaterMark Setting",
+    "setting": {
+      "name": "Mark Name",
+      "size": "Mark Size",
+      "color": "Color",
+      "angle": "Mark Angle",
+      "position": {
+        "label": "Mark Position",
+        "lt": "Left Top",
+        "rt": "Right Top",
+        "lb": "Left Bottom",
+        "rb": "Right Bottom",
+        "full": "Full"
+      }
+    }
+  },
+  "material": {
+    "cartoon": "cartoon"
+  },
+  "filters": {
+    "simple": "Simple Filter",
+    "complex": "Complex Filter",
+    "Invert": "Invert",
+    "Sepia": "Sepia",
+    "BlackWhite": "BlackWhite",
+    "Brownie": "Brownie",
+    "Vintage": "Vintage",
+    "Kodachrome": "Kodachrome",
+    "technicolor": "technicolor",
+    "Polaroid": "Polaroid",
+    "Grayscale": "Grayscale",
+    "GrayscaleList": {
+      "average": "average",
+      "lightness": "lightness",
+      "luminosity": "luminosity"
+    },
+    "RemoveColor": "RemoveColor",
+    "Brightness": "Brightness",
+    "Contrast": "Contrast",
+    "Saturation": "Saturation",
+    "Vibrance": "Vibrance",
+    "HueRotation": "HueRotation",
+    "Noise": "Noise",
+    "Pixelate": "Pixelate",
+    "Blur": "Blur",
+    "Gamma": "Gamma"
+  },
+  "flip": {
+    "x": "flip x",
+    "y": "flip y"
+  },
+  "center_align": {
+    "centerX": "centerX",
+    "center": "center",
+    "centerY": "centerY"
+  },
+  "group_align": {
+    "left": "left",
+    "centerX": "centerX",
+    "right": "right",
+    "top": "top",
+    "bottom": "bottom",
+    "centerY": "centerY",
+    "averageX": "averageX",
+    "averageY": "averageY"
+  },
+  "insert": "Insert",
+  "insertFile": {
+    "insert": "insert",
+    "insert_picture": "Insert picture",
+    "insert_SVG": "Insert SVG",
+    "insert_PSD": "insert PSD",
+    "insert_SVGStr": "Insert SVG String",
+    "insert_SVGStr_placeholder": "Please enter SVG String",
+    "modal_tittle": "Please enter"
+  },
+  "history": {
+    "revocation": "revocation",
+    "redo": "redo"
+  },
+  "select_image": "Select image",
+  "upload_background": "Upload background",
+  "mouseMenu": {
+    "layer": "LayerManage",
+    "copy": "Copy",
+    "delete": "Delete",
+    "group": "group",
+    "unGroup": "Split",
+    "up": "up",
+    "down": "down",
+    "upTop": "BringToFront",
+    "downTop": "SendToBack",
+    "center": "Center"
+  },
+  "alert": {
+    "loading_fonts": "Loading fonts, please wait...",
+    "loading_fonts_failed": "Fonts failed to load, please try again",
+    "loading_fonts_success": "Fonts success to load锛�",
+    "loading_data": "Loading data...",
+    "select_image": "Please select a background image",
+    "select_file": "Select a file",
+    "copied_sucessful": "澶嶅埗鎴愬姛",
+    "fail_copy": "澶嶅埗澶辫触"
+  },
+  "fruits": "Fruits",
+  "sports": "Sports",
+  "seasons": "Autumn",
+  "eletronics": "Computer",
+  "clothes": "Clothes",
+  "flags": "Flags",
+  "threes": "trees",
+  "food": "food",
+  "medals": "Medals",
+  "business": "Business",
+  "activity": "Activity",
+  "vintage": "vintage",
+  "animals": "animals",
+  "hand_painted": "hand painted",
+  "scenary_x": "Scenary {number}",
+  "color": "Color",
+  "red_book_vertical": "Red Book - V",
+  "red_book_horizontal": "Red Book - H",
+  "phone_wallpaper": "Phone Wallpaper",
+  "attributes": {
+    "id": "ID",
+    "linkData": "linkData",
+    "font": "Font",
+    "align": "Align",
+    "bold": "Bold:",
+    "italic": "Italic:",
+    "underline": "Underline:",
+    "stroke": "Stroke:",
+    "swipe_up": "Swipe UP:",
+    "line_height": "Line height",
+    "char_spacing": "Spacing",
+    "exterior": "Exterior",
+    "angle": "Angle",
+    "left": "Left",
+    "top": "Top",
+    "opacity": "Opacity",
+    "shadow": "Shadow",
+    "blur": "Blur",
+    "offset_x": "X",
+    "offset_y": "Y",
+    "borderRadiusX": "radiusX",
+    "borderRadiusY": "radiusY",
+    "rx_ry": "Rounded"
+  },
+  "setSizeTip": "Resize canvas",
+  "tip": "tip",
+  "clearTip": "Are you sure you want to clear it?",
+  "replaceTip": "Are you sure you want to add to the canvas?",
+  "ok": "ok",
+  "cancel": "cancel",
+  "cleanUp": "cleanUp",
+  "mine": "Mine"
+}
diff --git a/src/fabric-editor/language/index.ts b/src/fabric-editor/language/index.ts
new file mode 100644
index 0000000..99a4773
--- /dev/null
+++ b/src/fabric-editor/language/index.ts
@@ -0,0 +1,48 @@
+/*
+ * @Author: June
+ * @Description:
+ * @Date: 2023-10-29 12:18:14
+ * @LastEditors: June
+ * @LastEditTime: 2023-11-01 12:01:24
+ */
+import { createI18n } from 'vue-i18n';
+import zh from 'view-ui-plus/dist/locale/zh-CN';
+import en from 'view-ui-plus/dist/locale/en-US'; //鏂扮増鏈妸'iview'鏀规垚'view-design'
+import US from './en.json';
+import CN from './zh.json';
+import { getLocal, setLocal } from '@/utils/local';
+import { LANG } from '@/config/constants/app';
+
+const messages = {
+  en: Object.assign(US, en), //灏嗚嚜宸辩殑鑻辨枃鍖呭拰iview鎻愪緵鐨勭粨鍚�
+  zh: Object.assign(CN, zh), //灏嗚嚜宸辩殑涓枃鍖呭拰iview鎻愪緵鐨勭粨鍚�
+};
+
+function getLocalLang() {
+  let localLang = getLocal(LANG);
+  if (!localLang) {
+    let defaultLang = navigator.language;
+    if (defaultLang) {
+      // eslint-disable-next-line prefer-destructuring
+      defaultLang = defaultLang.split('-')[0];
+      // eslint-disable-next-line prefer-destructuring
+      localLang = defaultLang.split('-')[0];
+    }
+    setLocal(LANG, defaultLang);
+  }
+  return localLang;
+}
+const lang = getLocalLang();
+
+const i18n = createI18n({
+  allowComposition: true,
+  globalInjection: true,
+  legacy: false,
+  locale: lang,
+  messages,
+});
+
+export default i18n;
+export const t = (key: any) => {
+  return i18n.global.t(key);
+};
diff --git a/src/fabric-editor/language/pt.json b/src/fabric-editor/language/pt.json
new file mode 100644
index 0000000..ff7aceb
--- /dev/null
+++ b/src/fabric-editor/language/pt.json
@@ -0,0 +1,128 @@
+{
+  "templates": "Modelos",
+  "elements": "Elementos",
+  "background": "Fundo",
+  "size": "Tamanho",
+  "width": "Largura",
+  "height": "Altura",
+  "grid": "Grid",
+  "common_elements": "Elementos comuns",
+  "draw_elements": "Elementos arrastre",
+  "color_macthing": "Sele莽茫o de cor",
+  "background_texture": "Textura de fundo",
+  "picture": "Imagem",
+  "everything_is_fine": "Est谩 tudo bem",
+  "everything_goes_well": "Tudo vai bem",
+  "cartoon": "Cartoon",
+  "default_size": "Tamanho padr茫o",
+  "preview": "Antevis茫o",
+  "empty": "Vazio",
+  "keep": "Salvar",
+  "copy_to_clipboard": "Copiar para a 谩rea de transfer锚ncia",
+  "save_as_picture": "Salvar como imagem",
+  "save_as_svg": "Salvar como SVG",
+  "save_as_json": "Salvar como JSON",
+  "layers": "Camadas",
+  "title_template": "Sem t铆tulo",
+  "insert_svg": "Inserir SVG",
+  "select_svg": "Selecionar arquivo SVG",
+  "please_choose": "Por favor, escolha",
+  "string": "String",
+  "file": "Arquivo",
+  "import_files": "Importar arquivos",
+  "select_json": "Selecione o arquivo JSON",
+  "insert": "Inserir",
+  "insert_picture": "Inserir imagem",
+  "select_image": "Selecionar arquivo de imagem",
+  "insertFile": {
+    "remarks": "鎻掑叆鏂囦欢",
+    "insert": "insert",
+    "insert_picture": "Insert picture",
+    "insert_SVG": "Insert SVG",
+    "insert_SVGStr": "Insert SVG String",
+    "insert_SVGStr_placeholder": "Please enter SVG String",
+    "modal_tittle": "Please enter"
+  },
+  "waterMark": {
+    "text": "WaterMark",
+    "modalTitle": "WaterMark Setting",
+    "setting": {
+        "name": "Mark Name",
+        "size": "MarK Size",
+        "angle": "Mark Angle",
+        "position": {
+            "label": "Mark Position",
+            "lt": "Left Top",
+            "rt": "Right Top",
+            "lb": "Left Bottom",
+            "rb": "Right Bottom",
+            "full": "Full"
+        }
+    }
+  },
+  "upload_background": "Carregar plano de fundo",
+  "mouseMenu": {
+    "layer": "Gest茫o de camadas",
+    "copy": "Copiar",
+    "delete": "Eliminar",
+    "group": "combinaci贸n",
+    "unGroup": "divida",
+    "up": "up",
+    "down": "down",
+    "upTop": "Traer al frente",
+    "downTop": "Enviar a volver",
+    "center": "centro"
+  },
+  "alert": {
+    "loading_fonts": "Carregando fontes, aguarde...",
+    "loading_fonts_failed": "Falha ao carregar as fontes, tente novamente",
+    "loading_fonts_success": "O tipo de letra foi carregado com sucesso锛�",
+    "loading_data": "Carregando dados...",
+    "select_image": "Por favor, selecione uma imagem de plano de fundo",
+    "select_file": "Selecione um arquivo",
+    "copied_sucessful": "A c贸pia foi bem sucedida",
+    "fail_copy": "a c贸pia falhou"
+  },
+  "fruits": "Frutas de desenhos animados",
+  "sports": "Esportes",
+  "seasons": "Outono",
+  "eletronics": "Computador",
+  "clothes": "Roupas",
+  "flags": "Bandeiras",
+  "threes": "脕rvores",
+  "food": "Comida",
+  "medals": "Medalhas",
+  "business": "Neg贸cios",
+  "activity": "Atividade",
+  "vintage": "vintage",
+  "animals": "animais",
+  "hand_painted": "pintado 脿 m茫o",
+  "scenary_x": "Cena {number}",
+  "color": "Cor",
+  "red_book_vertical": "Red Book - V",
+  "red_book_horizontal": "Red Book - H",
+  "phone_wallpaper": "Phone Wallpaper",
+  "attributes": {
+    "id": "ID",
+    "font": "Fonte",
+    "align": "Alinhamento",
+    "bold": "Negrito:",
+    "italic": "Italico:",
+    "underline": "Underline:",
+    "stroke": "Stroke:",
+    "swipe_up": "Swipe UP:",
+    "line_height": "Line height",
+    "char_spacing": "Espa莽o Char.",
+    "exterior": "Exterior",
+    "angle": "脗ngulo",
+    "left": "Esq",
+    "top": "Topo",
+    "opacity": "Transpar锚ncia",
+    "shadow": "Sombra",
+    "blur": "Blur",
+    "offset_x": "X",
+    "offset_y": "Y",
+    "rx_ry": "Rounded",
+    "picture_filter": "Filtro"
+  }
+}
\ No newline at end of file
diff --git a/src/fabric-editor/language/zh.json b/src/fabric-editor/language/zh.json
new file mode 100644
index 0000000..e9f3281
--- /dev/null
+++ b/src/fabric-editor/language/zh.json
@@ -0,0 +1,263 @@
+{
+  "bgSeting": {
+    "size": "灏哄",
+    "color": "棰滆壊",
+    "colorMacthing": "閰嶈壊",
+    "width": "瀹藉害",
+    "height": "楂樺害"
+  },
+  "flip": {
+    "x": "姘村钩缈昏浆",
+    "y": "鍨傜洿缈昏浆"
+  },
+  "attrSeting": {
+    "group": "鎴愮粍",
+    "unGroup": "鎷嗗垎缁�",
+    "flip": {
+      "name": "缈昏浆",
+      "x": "姘村钩缈昏浆",
+      "y": "鍨傜洿缈昏浆"
+    },
+    "align": {
+      "name": "瀵归綈",
+      "left": "宸﹀榻�",
+      "centerX": "姘村钩灞呬腑瀵归綈",
+      "right": "鍙冲榻�",
+      "top": "涓婂榻�",
+      "bottom": "涓嬪榻�",
+      "centerY": "鍨傜洿灞呬腑瀵归綈",
+      "averageX": "姘村钩鍒嗗竷",
+      "averageY": "鍨傜洿鍒嗗竷"
+    },
+    "centerAlign": {
+      "name": "灞呬腑",
+      "centerX": "姘村钩灞呬腑",
+      "center": "姘村钩鍨傜洿灞呬腑",
+      "centerY": "鍨傜洿灞呬腑"
+    }
+  },
+  "color": "棰滆壊",
+  "textFloat": "灏忔暟",
+  "templates": "妯℃澘",
+  "elements": "鍏冪礌",
+  "font_style": "鏂囧瓧",
+  "background": "鑳屾櫙",
+  "grid": "缃戞牸",
+  "placeholder": "璇疯緭鍏�",
+  "common_elements": "鍩虹瑕佺礌",
+  "draw_elements": "缁樺埗鍏冪礌",
+  "code_img": "鏉$爜鍥剧墖",
+  "background_texture": "鑳屾櫙绾圭悊",
+  "picture": "鍥剧墖",
+  "everything_is_fine": "涓囦簨澶у悏",
+  "everything_goes_well": "璇镐簨椤洪亗",
+  "cartoon": "鍗¢��",
+  "default_size": "棰勮灏哄",
+  "preview": "棰勮",
+  "save": {
+    "empty": "娓呯┖",
+    "down": "涓嬭浇",
+    "save_my_spase": "淇濆瓨鍒版垜鐨勭┖闂�",
+    "save_as_picture": "PNG鍥剧墖",
+    "save_as_svg": "SVG鍥剧墖",
+    "copy_to_clipboard": "澶嶅埗鍒板壀鍒囨澘",
+    "copy_to_clipboardstr": "澶嶅埗瀛楃涓插壀鍒囨澘",
+    "save_as_json": "淇濆瓨涓篔SON"
+  },
+  "layers": "鍥惧眰",
+  "my_spase": {
+    "remove_templ": "鍒犻櫎妯℃澘",
+    "remove_templTip": "纭鍒犻櫎妯℃澘鍚楋紵",
+    "remove_file_type": "纭鍒犻櫎鏂囦欢澶瑰悧锛�",
+    "remove_file_type_Tip": "纭鍒犻櫎妯℃澘鍚楋紵"
+  },
+  "title_template": "鏍囬妯℃澘",
+  "insert_svg": "鎻掑叆SVG鍏冪礌",
+  "select_svg": "閫夋嫨SVG鏂囦欢",
+  "please_choose": "璇烽�夋嫨",
+  "string": "瀛楃涓�",
+  "file": "鏂囦欢",
+  "importFiles": {
+    "file": "鏂囦欢",
+    "createDesign": {
+      "title": "鍒涘缓璁捐",
+      "customSize": "鑷畾涔夊昂瀵�",
+      "systemSize": "绯荤粺鎺ㄨ崘灏哄",
+      "width": "瀹藉害",
+      "height": "楂樺害",
+      "create": "纭畾"
+    },
+    "importFiles": "瀵煎叆鏂囦欢"
+  },
+  "select_json": "閫夋嫨JSON鏂囦欢",
+  "repleaceImg": "鏇挎崲鍥剧墖",
+  "cropperImg": "瑁佸壀鍥剧墖",
+  "createClip": "娣诲姞瑁佸垏",
+  "removeClip": "绉婚櫎瑁佸垏",
+  "polygonClip": "澶氳竟褰�",
+  "rectClip": "鐭╁舰",
+  "circleClip": "鍦�",
+  "triangleClip": "涓夎褰�",
+  "polygonClipInverted": "澶氳竟褰€�愬弽銆�",
+  "rectClipInverted": "鐭╁舰銆愬弽銆�",
+  "circleClipInverted": "鍦嗐�愬弽銆�",
+  "triangleClipInverted": "涓夎褰€�愬弽銆�",
+  "waterMark": {
+    "text": "姘村嵃",
+    "modalTitle": "閰嶇疆姘村嵃",
+    "setting": {
+      "name": "姘村嵃鍚嶇О",
+      "size": "姘村嵃澶у皬",
+      "color": "姘村嵃棰滆壊",
+      "angle": "姘村嵃瑙掑害",
+      "position": {
+        "label": "姘村嵃浣嶇疆",
+        "lt": "宸︿笂瑙�",
+        "rt": "鍙充笂瑙�",
+        "lb": "宸︿笅瑙�",
+        "rb": "鍙充笅瑙�",
+        "full": "骞抽摵"
+      }
+    }
+  },
+  "material": {
+    "cartoon": "绱犳潗"
+  },
+  "filters": {
+    "simple": "绠�鍗曟护闀�",
+    "complex": "澶嶆潅婊ら暅",
+    "Invert": "鍙嶅悜",
+    "Sepia": "涔岄粦",
+    "BlackWhite": "榛戠櫧",
+    "Brownie": "宸у厠鍔�",
+    "Vintage": "澶嶅彜",
+    "Kodachrome": "鑳剁墖",
+    "technicolor": "椴滆壋",
+    "Polaroid": "楂樺厜",
+    "Grayscale": "鐏板害",
+    "GrayscaleList": {
+      "average": "涓�鑸�",
+      "lightness": "涓害",
+      "luminosity": "鏄庝寒"
+    },
+    "RemoveColor": "鍘婚櫎棰滆壊",
+    "Brightness": "浜害",
+    "Contrast": "瀵规瘮搴�",
+    "Saturation": "楗卞拰搴�",
+    "Vibrance": "鎸姩",
+    "HueRotation": "鑹茶皟",
+    "Noise": "鍣煶",
+    "Pixelate": "鍍忕礌鍖�",
+    "Blur": "妯$硦",
+    "Gamma": "璋冩暣"
+  },
+  "quick": {
+    "del": "鍒犻櫎",
+    "copy": "澶嶅埗",
+    "lock": "閿佸畾",
+    "unlock": "瑙i攣",
+    "hide": "闅愯棌",
+    "editPoly": "缂栬緫澶氳竟褰�"
+  },
+  "insertFile": {
+    "insert": "鎻掑叆",
+    "insert_picture": "鎻掑叆鍥剧墖",
+    "insert_SVG": "鎻掑叆SVG鍏冪礌",
+    "insert_SVGStr": "鎻掑叆SVG瀛楃",
+    "insert_PSD": "鎻掑叆PSD",
+    "insert_SVGStr_placeholder": "璇疯緭鍏VG瀛楃涓�",
+    "modal_tittle": "璇疯緭鍏�"
+  },
+  "history": {
+    "revocation": "鎾ら攢",
+    "redo": "閲嶅仛"
+  },
+  "upload_background": "涓婁紶鑳屾櫙",
+  "mouseMenu": {
+    "layer": "鍥惧眰绠$悊",
+    "copy": "澶嶅埗",
+    "delete": "鍒犻櫎",
+    "group": "缁勫悎",
+    "unGroup": "鍙栨秷缁勫悎",
+    "up": "涓婄Щ涓�灞�",
+    "down": "涓嬬Щ涓�灞�",
+    "upTop": "绉诲埌椤堕儴",
+    "downTop": "绉诲埌搴曢儴",
+    "center": "姘村钩鍨傜洿灞呬腑"
+  },
+  "alert": {
+    "loading_fonts": "姝e湪鍔犺浇瀛椾綋锛屾偍鑰愬績绛夊��...",
+    "loading_fonts_failed": "瀛椾綋鍔犺浇澶辫触锛岃閲嶈瘯!",
+    "loading_fonts_success": "瀛椾綋鍔犺浇鎴愬姛锛�",
+    "loading_data": "鍔犺浇鏁版嵁涓�...",
+    "select_image": "璇烽�夋嫨鑳屾櫙鍥剧墖",
+    "select_file": "璇烽�夋嫨鏂囦欢",
+    "copied_sucessful": "澶嶅埗鎴愬姛",
+    "fail_copy": "澶嶅埗澶辫触"
+  },
+  "scenary_x": "鏂规 {number}",
+  "red_book_vertical": "绾功绔栫増",
+  "red_book_horizontal": "绾功妯増",
+  "phone_wallpaper": "鎵嬫満澹佺焊",
+  "attributes": {
+    "id": "鏍囪瘑",
+    "linkData": "鍏宠仈",
+    "font": "瀛椾綋",
+    "align": "瀵归綈",
+    "bold": "鍔犵矖:",
+    "italic": "鏂滀綋:",
+    "underline": "涓嬪垝:",
+    "stroke": "杈规",
+    "swipe_up": "涓婂垝:",
+    "line_height": "琛岄珮",
+    "char_spacing": "闂磋窛",
+    "exterior": "澶栬",
+    "angle": "鏃嬭浆",
+    "left": "X杞�",
+    "top": "Y杞�",
+    "opacity": "閫忔槑",
+    "shadow": "闃村奖",
+    "blur": "妯$硦",
+    "offset_x": "X杞�",
+    "offset_y": "Y杞�",
+    "borderRadiusX": "鍦嗚姘村钩鍗婂緞",
+    "borderRadiusY": "鍦嗚鍨傜洿鍗婂緞",
+    "rx_ry": "鍗婂緞"
+  },
+  "setSizeTip": "璋冩暣鐢诲竷灏哄",
+  "tip": "鎻愮ず",
+  "clearTip": "纭畾瑕佹竻绌哄悧锛�",
+  "replaceTip": "纭畾瑕佹坊鍔犲埌鐢诲竷涓悧锛�",
+  "ok": "纭",
+  "cancel": "鍙栨秷",
+  "cleanUp": "娓呴櫎",
+  "login": {
+    "title": "娆㈣繋浣跨敤",
+    "welcome": "娆㈣繋浣跨敤蹇浘",
+    "login": "鐧诲綍",
+    "register": "娉ㄥ唽",
+    "identifier": "鐢ㄦ埛鍚�",
+    "username": "鐢ㄦ埛鍚�",
+    "password": "瀵嗙爜",
+    "email": "閭",
+    "identifierValidate": "璇疯緭鍏ョ敤鎴峰悕",
+    "passwordValidate": "璇疯緭鍏�6浣嶄互涓婄殑瀵嗙爜",
+    "emailValidate": "璇疯緭鍏ユ纭殑閭鍦板潃",
+    "logoutTip": "纭畾瑕侀��鍑哄悧锛�",
+    "logoutSuccessTip": "閫�鍑烘垚鍔�"
+  },
+  "myMaterial": {
+    "uploadBtn": "涓婁紶绱犳潗"
+  },
+  "admin": {
+    "btnTitle": "绠$悊",
+    "setToken": "璁剧疆Token",
+    "save": "淇濆瓨",
+    "create": "鏂板",
+    "addTempl": "鏂板妯℃澘",
+    "addTemplPlaceholder": "璇疯緭鍏ユā鏉垮悕绉�",
+    "addTemplCheckTip": "妯℃澘鍚嶇О涓嶈兘涓虹┖"
+  },
+  "mine": "鎴戠殑",
+  "batch": "鎵归噺"
+}
\ No newline at end of file
diff --git a/src/fabric-editor/plugin/MyHistoryPlugin.ts b/src/fabric-editor/plugin/MyHistoryPlugin.ts
new file mode 100644
index 0000000..ff9428d
--- /dev/null
+++ b/src/fabric-editor/plugin/MyHistoryPlugin.ts
@@ -0,0 +1,10 @@
+//@ts-nocheck
+import { HistoryPlugin } from '@muaitu/fabric-editor-core';
+
+export class MyHistoryPlugin extends HistoryPlugin {
+  _init() {
+    this.canvas.on('history:append', () => {
+      this.historyUpdate();
+    });
+  }
+}
diff --git a/src/fabric-editor/plugin/fabric-history.js b/src/fabric-editor/plugin/fabric-history.js
new file mode 100644
index 0000000..4924c44
--- /dev/null
+++ b/src/fabric-editor/plugin/fabric-history.js
@@ -0,0 +1,224 @@
+import { fabric } from 'fabric';
+
+fabric.Canvas.prototype.initialize = (function (originalFn) {
+  return function (...args) {
+    originalFn.call(this, ...args);
+    this._historyInit();
+    return this;
+  };
+})(fabric.Canvas.prototype.initialize);
+
+/**
+ * Override the dispose function for the _historyDispose();
+ */
+fabric.Canvas.prototype.dispose = (function (originalFn) {
+  return function (...args) {
+    originalFn.call(this, ...args);
+    this._historyDispose();
+    return this;
+  };
+})(fabric.Canvas.prototype.dispose);
+
+/**
+ * Returns current state of the string of the canvas
+ */
+fabric.Canvas.prototype._historyNext = function () {
+  return JSON.stringify(this.toDatalessJSON(this.extraProps));
+};
+
+/**
+ * Returns an object with fabricjs event mappings
+ */
+fabric.Canvas.prototype._historyEvents = function () {
+  return {
+    'object:added': (e) => this._historySaveAction(e),
+    'object:removed': (e) => this._historySaveAction(e),
+    'object:modified': (e) => this._historySaveAction(e),
+    'object:skewing': (e) => this._historySaveAction(e),
+  };
+};
+
+/**
+ * Initialization of the plugin
+ */
+fabric.Canvas.prototype._historyInit = function () {
+  this.historyStack = [];
+  this.historyIndex = 0;
+  this.historyMaxLength = 100;
+  this.extraProps = [
+    'id',
+    'gradientAngle',
+    'selectable',
+    'hasControls',
+    'linkData',
+    'editable',
+    'extensionType',
+    'extension',
+  ];
+  this.historyNextState = this._historyNext();
+  // 闇�瑕佷袱娆℃搷浣滅殑鏍囪锛屼负true鏃惰〃绀哄綋鍓嶆搷浣滆褰曚负鏈�鏂拌褰曪紝闇�瑕佹挙閿�涓ゆ锛屽洜涓烘渶椤跺眰鐨勬槸褰撳墠鐨勬渶鏂拌褰曪紝undo涓�娆″悗鍚庣疆涓篺alse
+  this.isLatestHistoryState = true;
+  // 姝e湪璇诲彇鍘嗗彶璁板綍鐨勬爣璁帮紝涓� true 鏃朵笉鍏佽 undo/redo
+  this.isLoadingHistory = false;
+
+  this.on(this._historyEvents());
+};
+
+/**
+ * Remove the custom event listeners
+ */
+fabric.Canvas.prototype._historyDispose = function () {
+  this.off(this._historyEvents());
+};
+
+/**
+ * It pushes the state of the canvas into history stack
+ */
+fabric.Canvas.prototype._historySaveAction = function (e) {
+  if (this.historyProcessing) return;
+  if (!e || (e.target && !e.target.excludeFromExport)) {
+    const json = this._historyNext();
+    // 褰撳墠鎿嶄綔璁板綍闈炴渶鏂拌褰曪紝鏇存柊璁板綍鍓嶉渶瑕佹牎姝e巻鍙茬储寮曪紝涓嶇劧浼氫涪澶变竴涓褰曪紙undo鏃舵挙閿�浜嗕袱娆¤褰曪級銆傜悊璁轰笂涓嶄細瓒呭嚭鍘嗗彶璁板綍涓婇檺锛屼笉杩囪繕鏄姞浜嗛檺鍒�
+    !this.isLatestHistoryState &&
+      (this.isLatestHistoryState = true) &&
+      this.historyIndex < this.historyMaxLength &&
+      this.historyIndex++;
+    // 姣忔鐨勬渶鏂版搷浣滈兘瑕佹竻绌哄巻鍙茬储寮曚箣鍚庣殑璁板綍锛岄槻姝edo鏃ц褰曪紝涓嶇劧鍙兘浼歳edo涔嬪墠鏌愪釜闃舵鐨勬搷浣滆褰�
+    this.historyStack.length > this.historyIndex && this.historyStack.splice(this.historyIndex);
+    // 鏈�澶氫繚瀛� historyMaxLength 鏉¤褰�
+    if (this.historyIndex >= this.historyMaxLength) this.historyStack.shift();
+    this.historyIndex < this.historyMaxLength && this.historyIndex++;
+    this.historyStack.push(json);
+    this.historyNextState = this._historyNext();
+    this.fire('history:append', { json });
+  }
+};
+
+/**
+ * Undo to latest history.
+ * Pop the latest state of the history. Re-render.
+ * Also, pushes into redo history.
+ */
+fabric.Canvas.prototype.undo = function (callback) {
+  if (this.isLoadingHistory) return;
+  if (this.historyIndex <= 0) return;
+  // The undo process will render the new states of the objects
+  // Therefore, object:added and object:modified events will triggered again
+  // To ignore those events, we are setting a flag.
+  this.historyProcessing = true;
+
+  // 褰撳墠鎿嶄綔璁板綍涓烘渶鏂拌褰曪紝闇�瑕佹挙閿�涓ゆ锛屽洜涓烘渶椤跺眰鐨勬槸褰撳墠鐨勬渶鏂拌褰�
+  this.isLatestHistoryState && this.historyIndex-- && (this.isLatestHistoryState = false);
+  const history = this.historyStack[--this.historyIndex];
+  if (history) {
+    // Push the current state to the redo history
+    this.historyNextState = history;
+    this._loadHistory(history, 'history:undo', callback);
+  } else {
+    console.log(1111);
+    this.historyIndex < 0 && (this.historyIndex = 0);
+    this.historyProcessing = false;
+  }
+};
+
+/**
+ * Redo to latest undo history.
+ */
+fabric.Canvas.prototype.redo = function (callback) {
+  if (this.isLoadingHistory) return;
+  if (this.historyIndex >= this.historyStack.length) return;
+  // The undo process will render the new states of the objects
+  // Therefore, object:added and object:modified events will triggered again
+  // To ignore those events, we are setting a flag.
+  this.historyProcessing = true;
+  // 褰撳墠鎿嶄綔璁板綍涓嶆槸鏈�鏂拌褰曪紙琚挙閿�杩囷級锛岄渶瑕佹仮澶嶄袱姝ワ紝鎶垫秷鏈�鍒濇挙閿�鏃舵挙閿�涓ゆ鐨勬搷浣�
+  !this.isLatestHistoryState && ++this.historyIndex && (this.isLatestHistoryState = true);
+  const history = this.historyStack[this.historyIndex];
+  if (history) {
+    // Every redo action is actually a new action to the undo history
+    this.historyNextState = history;
+    this._loadHistory(history, 'history:redo', callback);
+    this.historyIndex++;
+  } else {
+    this.historyProcessing = false;
+  }
+};
+
+// loadFromJSON 鏄紓姝ユ搷浣滐紝鎵�浠ラ�氳繃 isLoadingHistory = true 琛ㄧず鍘嗗彶璇诲彇涓紝涓嶅彲 undo/redo锛�
+// 涓嶇劧褰撻〉闈㈠鏉備笖蹇�� undo/redo 澶氭鍚庯紝鍙兘浼氬湪涔嬪墠鐨勫巻鍙蹭笂 redo/undo
+fabric.Canvas.prototype._loadHistory = function (history, event, callback) {
+  this.isLoadingHistory = true;
+  const that = this;
+
+  // 闇�瑕佹妸鍘嗗彶璁板綍涓殑 workspace 鐨� evented 灞炴�ц缃负 false锛屽惁鍒欎細瀵艰嚧鍘嗗彶璁板綍鎭㈠鍚庯紝榧犳爣鎮诞 workspace 鍑虹幇鍙搷浣滅殑鏍峰紡
+  const workspaceHistory = history.objects?.find((item) => item.id === 'workspace');
+  workspaceHistory && (workspaceHistory.evented = false);
+
+  this.loadFromJSON(history, function () {
+    that.renderAll();
+    that.fire(event);
+    that.historyProcessing = false;
+    that.isLoadingHistory = false;
+
+    if (callback && typeof callback === 'function') callback();
+  });
+};
+
+/**
+ * Clear undo and redo history stacks
+ */
+fabric.Canvas.prototype.clearHistory = function (type) {
+  const one = this.historyStack.pop();
+  if (!type || !one) {
+    this.historyStack = [];
+    this.historyIndex = 0;
+    this.fire('history:clear');
+  } else {
+    this.historyStack = [one];
+    this.historyIndex = 1;
+    this.fire('history:clear');
+  }
+  this.isLatestHistoryState = true;
+};
+
+fabric.Canvas.prototype.clearUndo = function () {
+  this.historyStack.splice(this.historyIndex);
+};
+
+// 濡傛灉鍦ㄥ仛涓�浜涙搷浣滀箣鍚庯紝闇�瑕佹挙閿�涓婁竴姝ョ殑鎿嶄綔骞跺埛鏂板巻鍙茶褰曪紙鎯冲湪鐩戝惉modified浜嬩欢鍚庡仛浜涢澶栫殑鎿嶄綔骞惰褰曟搷浣滃悗鐨勫巻鍙诧級锛屽彲浠ヨ皟鐢ㄨ繖涓柟娉�
+fabric.Canvas.prototype.refreshHistory = function () {
+  this.historyProcessing = false;
+  this.historyStack.splice(--this.historyIndex);
+  this._historySaveAction();
+};
+
+/**
+ * On the history
+ */
+fabric.Canvas.prototype.onHistory = function () {
+  this.historyProcessing = false;
+
+  this._historySaveAction();
+};
+
+/**
+ * Check if there are actions that can be undone
+ */
+
+fabric.Canvas.prototype.canUndo = function () {
+  return this.historyIndex > 0;
+};
+
+/**
+ * Check if there are actions that can be redone
+ */
+fabric.Canvas.prototype.canRedo = function () {
+  return this.historyStack.length > this.historyIndex;
+};
+
+/**
+ * Off the history
+ */
+fabric.Canvas.prototype.offHistory = function () {
+  this.historyProcessing = true;
+};
diff --git a/src/fabric-editor/styles/contextMenu.scss b/src/fabric-editor/styles/contextMenu.scss
new file mode 100644
index 0000000..58fb36b
--- /dev/null
+++ b/src/fabric-editor/styles/contextMenu.scss
@@ -0,0 +1,56 @@
+.context {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 999;
+  display: inline-block;
+  padding: 3px 0;
+  min-width: 270px;
+  font-size: 9pt;
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+  border: 1px solid #333333;
+  border-radius: 6px;
+  color: #ffffff;
+  background: #262933;
+  box-shadow: 2px 2px 2px -1px rgb(0 0 0 / 50%);
+  -webkit-touch-callout: none;
+  user-select: none;
+}
+
+.context .item {
+  padding: 4px 19px;
+  cursor: default;
+  color: inherit;
+}
+
+.context .item:hover {
+  background: #2777ff;
+}
+
+.context .item:hover .hotkey {
+  color: #ffffff;
+}
+
+.context .disabled {
+  color: #878b90;
+}
+
+.context .disabled:hover {
+  background: inherit;
+}
+
+.context .disabled:hover .hotkey {
+  color: #878b90;
+}
+
+.context .separator {
+  margin: 4px 0;
+  padding: 0;
+  height: 0;
+  border-top: 1px solid #454545;
+}
+
+.hotkey {
+  float: right;
+  color: #878b90;
+}
diff --git a/src/fabric-editor/styles/index.less b/src/fabric-editor/styles/index.less
new file mode 100644
index 0000000..1bb4c48
--- /dev/null
+++ b/src/fabric-editor/styles/index.less
@@ -0,0 +1,32 @@
+// @import '~view-ui-plus/src/styles/index.less';
+@import './resetViewUi.less';
+// view-ui iconfont (404闂 https://github.com/view-design/ViewUIPlus/issues/212)
+// @ionicons-font-path: '~view-ui-plus/src/styles/common/iconfont/fonts';
+// @primary-color: #8c0776;
+
+div.attr-item-box {
+  //   padding-bottom: 20px;
+
+  h3 {
+    padding-bottom: 10px;
+  }
+
+  .ivu-tooltip {
+    flex: 1;
+  }
+
+  .ivu-tooltip-rel {
+    width: 100%;
+  }
+
+  .bg-item {
+    display: flex;
+    justify-content: space-between;
+    // margin-bottom: 5px;
+    padding: 5px;
+    width: 100%;
+    border-radius: 5px;
+    background: #f6f7f9;
+    flex: 1;
+  }
+}
diff --git a/src/fabric-editor/styles/resetViewUi.less b/src/fabric-editor/styles/resetViewUi.less
new file mode 100644
index 0000000..74a1770
--- /dev/null
+++ b/src/fabric-editor/styles/resetViewUi.less
@@ -0,0 +1,53 @@
+// viewUi涓璪uttonGroup涓嬪嚑涓猙utton鐨刦ocus鐘舵�佷笅鏍峰紡瀛樺湪闂锛屽悗杈圭殑浼氳鐩栧墠杈圭殑box-shadow,杩欓噷缁檉ocus鍏冪礌娣诲姞z-index.
+
+.ivu-btn:focus {
+  z-index: 2;
+  box-shadow: 0 0 0 2px rgb(45 140 240 / 20%);
+}
+
+.preview-modal-wrap {
+  overflow: hidden;
+
+  .ivu-modal-body {
+    display: flex;
+    //婊氬姩鏉$Щ鍔ㄤ笂鍘绘墠灞曠ず锛岀Щ寮�涓嶅睍绀烘粴鍔ㄦ潯
+    overflow: overlay;
+    padding: 0;
+    max-height: calc(100vh - 51px);
+
+    &::-webkit-scrollbar {
+      width: 4px;
+      background-color: #efeae6;
+    }
+
+    &:hover ::-webkit-scrollbar-track-piece {
+      /* 婊氬姩鏉$殑鑳屾櫙棰滆壊 */
+      border-radius: 6px;
+
+      /* 榧犳爣绉诲姩涓婂幓鍐嶆樉绀烘粴鍔ㄦ潯 */
+      background-color: #ffffff;
+
+      /* 婊氬姩鏉$殑鍦嗚瀹藉害 */
+    }
+
+    &:hover::-webkit-scrollbar-thumb:hover {
+      background-color: #c1c1c1;
+    }
+
+    &:hover::-webkit-scrollbar-thumb:vertical {
+      border: 2px solid #c1c1c1;
+      border-radius: 6px;
+      background-color: #c1c1c1;
+      outline: 2px solid #c1c1c1;
+      outline-offset: -2px;
+    }
+  }
+
+  .ivu-modal-content {
+    border-radius: 6px 6px 0 0;
+  }
+
+  .ivu-modal {
+    top: 0;
+  }
+}
diff --git a/src/fabric-editor/styles/resizePlugin.scss b/src/fabric-editor/styles/resizePlugin.scss
new file mode 100644
index 0000000..9c31e1e
--- /dev/null
+++ b/src/fabric-editor/styles/resizePlugin.scss
@@ -0,0 +1,33 @@
+.resize-bar {
+  position: absolute;
+  border-radius: 3px;
+  background-color: rgb(160 160 160 / 60%);
+}
+
+.resize-bar:hover {
+  background-color: #328cff;
+}
+
+.resize-bar.horizontal {
+  width: 6px;
+  height: 30px;
+}
+
+.resize-bar.vertical {
+  width: 30px;
+  height: 6px;
+}
+
+.resize-bar.active {
+  background-color: #328cff;
+}
+
+#resize-left-bar,
+#resize-right-bar {
+  cursor: ew-resize;
+}
+
+#resize-top-bar,
+#resize-bottom-bar {
+  cursor: ns-resize;
+}
diff --git a/src/fabric-editor/styles/variable.less b/src/fabric-editor/styles/variable.less
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/fabric-editor/styles/variable.less
diff --git a/src/fabric-editor/types.d.ts b/src/fabric-editor/types.d.ts
new file mode 100644
index 0000000..f7b6484
--- /dev/null
+++ b/src/fabric-editor/types.d.ts
@@ -0,0 +1,19 @@
+declare module '@types/fabric/fabric-impl.d.ts' {
+  export interface  ITextOptions {
+    templateDataParamId?: string;
+
+    pageNum?: number;
+
+    templateParamType?: EnumContractTemplateValueType;
+    recorder?: EnumContractTemplateValueRecorder;
+    userType?: EnumUserType;
+    /** 鍙橀噺鍚嶇О */
+    label?: string;
+    /** 鍙橀噺浠g爜 */
+    name?: string;
+    /** 鏄惁蹇呭~ */
+    required?: boolean;
+  }
+}
+
+export {}
diff --git a/src/fabric-editor/utils/index.ts b/src/fabric-editor/utils/index.ts
new file mode 100644
index 0000000..d55eaee
--- /dev/null
+++ b/src/fabric-editor/utils/index.ts
@@ -0,0 +1,32 @@
+import { TemplateParamObjectName, TemplateParam, TemplateParamExtensionKey } from '../customObject';
+import { flattenDeep } from 'lodash';
+import { fabric } from 'fabric';
+import { Message } from '@bole-core/core';
+
+export type CanvasJson = ReturnType<fabric.StaticCanvas['toJSON']> & {
+  backgroundImage?: fabric.Image;
+};
+
+export function convertJsonMapToTemplateParamObjectList(jsonMap: Record<number, CanvasJson>) {
+  const jsonList = Object.values(jsonMap).map((x) =>
+    x.objects.filter((item) => item.type === TemplateParamObjectName)
+  );
+  return flattenDeep(jsonList) as TemplateParam[];
+}
+
+export function checkTemplateParamObjectListNotNull(templateParamList: TemplateParam[]) {
+  for (const templateParam of templateParamList) {
+    const nullTemplateParamKey = TemplateParamExtensionKey.find(
+      (key) =>
+        !templateParam[key] &&
+        templateParam[key] !== false &&
+        key !== 'pageNum' &&
+        key !== 'templateDataParamId'
+    );
+    if (nullTemplateParamKey) {
+      Message.warnMessage(`妯℃澘鍙傛暟${templateParam.label}瀛樺湪蹇呭~灞炴�т负绌篳);
+      return false;
+    }
+  }
+  return true;
+}
diff --git a/src/fabric-editor/utils/local.ts b/src/fabric-editor/utils/local.ts
new file mode 100644
index 0000000..e7a4daf
--- /dev/null
+++ b/src/fabric-editor/utils/local.ts
@@ -0,0 +1,36 @@
+/**
+ * get localStorage 鑾峰彇鏈湴瀛樺偍
+ * @param { String } key
+ */
+export function getLocal(key: string) {
+  if (!key) throw new Error('key is empty');
+  const value = localStorage.getItem(key);
+  return value ? JSON.parse(value) : null;
+}
+
+/**
+ * set localStorage 璁剧疆鏈湴瀛樺偍
+ * @param { String } key
+ * @param value
+ */
+export function setLocal(key: string, value: unknown) {
+  if (!key) throw new Error('key is empty');
+  if (!value) return;
+  return localStorage.setItem(key, JSON.stringify(value));
+}
+
+/**
+ * remove localStorage 绉婚櫎鏌愪釜鏈湴瀛樺偍
+ * @param { String } key
+ */
+export function removeLocal(key: string) {
+  if (!key) throw new Error('key is empty');
+  return localStorage.removeItem(key);
+}
+
+/**
+ * clear localStorage 娓呴櫎鏈湴瀛樺偍
+ */
+export function clearLocal() {
+  return localStorage.clear();
+}
diff --git a/src/fabric-editor/utils/math.ts b/src/fabric-editor/utils/math.ts
new file mode 100644
index 0000000..012c4c0
--- /dev/null
+++ b/src/fabric-editor/utils/math.ts
@@ -0,0 +1,25 @@
+/**
+ * 鑾峰彇澶氳竟褰㈤《鐐瑰潗鏍�
+ * @param edges 鍙樻暟
+ * @param radius 鍗婂緞
+ * @returns 鍧愭爣鏁扮粍
+ */
+const getPolygonVertices = (edges: number, radius: number) => {
+  const vertices = [];
+  const interiorAngle = (Math.PI * 2) / edges;
+  let rotationAdjustment = -Math.PI / 2;
+  if (edges % 2 === 0) {
+    rotationAdjustment += interiorAngle / 2;
+  }
+  for (let i = 0; i < edges; i++) {
+    // 鐢诲渾鍙栭《鐐瑰潗鏍�
+    const rad = i * interiorAngle + rotationAdjustment;
+    vertices.push({
+      x: Math.cos(rad) * radius,
+      y: Math.sin(rad) * radius,
+    });
+  }
+  return vertices;
+};
+
+export { getPolygonVertices };
diff --git a/src/fabric-editor/viewcomponents/left/index.vue b/src/fabric-editor/viewcomponents/left/index.vue
new file mode 100644
index 0000000..5694878
--- /dev/null
+++ b/src/fabric-editor/viewcomponents/left/index.vue
@@ -0,0 +1,123 @@
+<script lang="ts" setup>
+// 宸︿晶缁勪欢
+// import tools from '@/fabric-editor/components/tools.vue';
+import systemTemplateDataParamSetting from '@/fabric-editor/components/systemTemplateDataParamSetting.vue';
+
+const state = reactive({
+  menuActive: 1,
+  toolsBarShow: true,
+});
+// 宸︿晶鑿滃崟娓叉煋
+const menuActive = ref('systemTemplateDataParamSetting');
+const leftBarComponent = {
+  systemTemplateDataParamSetting,
+};
+
+const leftBar = reactive([
+  {
+    //绯荤粺妯℃澘鍙傛暟
+    key: 'systemTemplateDataParamSetting',
+    name: computed(() => '绯荤粺妯℃澘鍙傛暟'),
+    icon: 'md-images',
+  },
+]);
+// 闅愯棌宸ュ叿鏉�
+const hideToolsBar = () => {
+  state.toolsBarShow = !state.toolsBarShow;
+};
+// 灞曠ず宸ュ叿鏉�
+const showToolsBar = (val: any) => {
+  menuActive.value = val;
+  state.toolsBarShow = true;
+};
+
+// onMounted(() => {
+//   // 鏈塈D鏃讹紝鎵撳紑浣滃搧闈㈡澘
+//   const route = useRoute();
+//   if (route?.query?.id) {
+//     menuActive.value = 'myMaterial';
+//   }
+// });
+</script>
+
+<template>
+  <div :class="`left-bar ${state.toolsBarShow && 'show-tools-bar'}`">
+    <div class="content" v-show="state.toolsBarShow">
+      <div class="left-panel">
+        <KeepAlive>
+          <component :is="leftBarComponent[menuActive]"></component>
+        </KeepAlive>
+      </div>
+    </div>
+    <!-- 鍏抽棴鎸夐挳 -->
+    <div
+      :class="`close-btn left-btn ${state.toolsBarShow && 'left-btn-open'}`"
+      @click="hideToolsBar"
+    ></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+// 宸︿晶瀹瑰櫒
+
+.left-bar {
+  position: relative;
+  display: flex;
+  width: 65px;
+  height: 100%;
+  background: #ffffff;
+
+  &.show-tools-bar {
+    width: 240px;
+  }
+}
+
+.ivu-menu-vertical .menu-item {
+  padding: 10px 2px;
+  font-size: 12px;
+  text-align: center;
+  box-sizing: border-box;
+
+  & > i {
+    margin: 0;
+  }
+}
+
+.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu) {
+  background: none;
+}
+
+.content {
+  overflow-y: auto;
+  padding: 0 10px;
+  width: 120px;
+  height: 100%;
+  flex: 1;
+}
+
+// 鍏抽棴鎸夐挳
+
+.close-btn {
+  position: absolute;
+  top: 50%;
+  right: -20px;
+  z-index: 1;
+  margin-top: -10px;
+  width: 20px;
+  height: 64px;
+  background-position: 50%;
+  background-repeat: no-repeat;
+  background-size: cover;
+  cursor: pointer;
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAMAAABOb9vcAAAAhFBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AAADHx8cODg50dHTx8fF2dnZ1dXWWlpZHR0c4ODhQpkZ5AAAAIXRSTlMA9t+/upkRAnPq5NXDfDEsKQjMeGlRThkMsquljTwzIWhBHpjgAAABJElEQVRYw+3YyW7CQBCEYbxig8ELGJyQkJRJyPb+75dj3zy/lD7kMH3+ZEuzSFO1mlZwhjOE2uwhVHJYMygNVwilhz2EUvNaMigledUFoE1anKYAtA9nVRuANpviOQBt0t2ZQSnZ9QxK6Qih9LSGUHkJobYlhGp6CPW4hlAVhckLhMop1InCjEK1FBYU1hSqo/BI4YXCjMIthTWFijDCCB3g7fuO4O1t/rkvQXPz/LUIzX0oAM0tQHOfCkBzC9DcuwLQXACao9Dv1yb9lsek2xaaxMcMH1x6Ff79dY0wwgj/DGv3p2tG4cX9wd55h4rCO/hk3uEs9w6QlXPIbXrfIJ6XrmVBOtJCA1YkXqVLkh1aUgyNk1fV1BxLxzpsuNLKzrME/AWr0ywwvyj83AAAAABJRU5ErkJggg==');
+
+  &.left-btn {
+    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAYAAAB5sSvuAAAAAXNSR0IArs4c6QAAAFBlWElmTU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAKKADAAQAAAABAAAAgAAAAAAobJzlAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAADf0lEQVR4Ae2cvYsTQRjGE7FQkICFB1pZRyzEJkUKmzOpBEHwX9DCQkmChf4JahewsLpWFOQUzwMRPEgEy0PLpPADvEISDrVyfZ6cK0tIZrI7u7MPMi+8mb35uPnlmXczyeXmrURRdKyibAB8Dz8pywg42if4OUnIGd7Bww8Ut+GHpEATgPEll/y8DGRMtaB8hrryl30B2HzVW1Rcgx8vQ9UqaVac+Cf67cC34C+q1erHFcc5dUsDOD/RGBWv4M/hrwG8jzJ3cwFMwlDdd/BN+BZgd5ONLtd5Ac4zfEYFld0ALMMisxUFmAQa44dHdMB+TTasdM2bxJNxI7gDP7ISWNzJE1xymhF+uBzPbyvL2NZOA+oJIO/BrfP7iEGTSNtovIrY/L6sU9mA5PoAby6DtEq87JnlWF/H7+K+v/DmUQDkc23CNxbFpAogIa/Ab/IiaQoxmOThlnkG8TiKK5UUJNNR+MMYjqUaIJnWEYuXeEFTBCTXv1hUi0HCxXYWsbirqiAhb/BBWcE9KLimDEgB68pLTMAL6oBNdcBT6oBr6oAn1O9i2a2Od/DM1Jc4KBivVOYyLHFm6f4ODAoGBV0VcB0fYjAo6KqA6/gQg0FBVwVcx4cYDAq6KuA6/v+Mwel0Wmm325XhcOgqkH08/h6cyiaTSdRoNPhvBFGtVosGg0Gq8Wk7V9IO6Pf7MzgC+oBMDcgn1Ov1vEFmAvQJmRmQkN1ut3AlnQB9QDoDErLT6RSmZC6ARULmBlgUpPxWl5uCRcVhLoBFwTFsnAGLfi10AiwazklBX/txJgV9wWVSUP7tlvwbVspOyFarVfi7ac4Vvquzfyoy95DfiwOgeQHtrUFBu0bmHkFBsz721qCgXSNzj6CgWR97a1DQrpG5R1DQrI+9NSho18jcIyho1sfauqeuoDzgN3UFv6gD7qh/cK8rA84OGygv8VO+CCkrKH3g5Q1P41BB1SV+QDia4hJvQ72LB3h6gPIH/+5CvVGsntoSPwYQzxr/VgRkJoF1wP1KwvFa4SaRPgDNI+RLT2dTwTJfB+9j/jaWden5dgIe5oNnG2O+WwCb7bXWuflliSfLlAjCh4JULHMqjaIAc0tGkhdgnM6FyXI2EV+5pXNxAeTSMSHOSzg3+H2UuVsaQKq0A/eaUmiVb9yZlOk6vJSkTCZA2bRWsonBpFOrySan+wNoJmOM0LyBGwAAAABJRU5ErkJggg==');
+  }
+
+  &.left-btn-open {
+    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAMAAABOb9vcAAAAhFBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AAADHx8cODg50dHTx8fF2dnZ1dXWWlpZHR0c4ODhQpkZ5AAAAIXRSTlMA9t+/upkRAnPq5NXDfDEsKQjMeGlRThkMsquljTwzIWhBHpjgAAABJElEQVRYw+3YyW7CQBCEYbxig8ELGJyQkJRJyPb+75dj3zy/lD7kMH3+ZEuzSFO1mlZwhjOE2uwhVHJYMygNVwilhz2EUvNaMigledUFoE1anKYAtA9nVRuANpviOQBt0t2ZQSnZ9QxK6Qih9LSGUHkJobYlhGp6CPW4hlAVhckLhMop1InCjEK1FBYU1hSqo/BI4YXCjMIthTWFijDCCB3g7fuO4O1t/rkvQXPz/LUIzX0oAM0tQHOfCkBzC9DcuwLQXACao9Dv1yb9lsek2xaaxMcMH1x6Ff79dY0wwgj/DGv3p2tG4cX9wd55h4rCO/hk3uEs9w6QlXPIbXrfIJ6XrmVBOtJCA1YkXqVLkh1aUgyNk1fV1BxLxzpsuNLKzrME/AWr0ywwvyj83AAAAABJRU5ErkJggg==');
+    transform: rotateY(360deg);
+  }
+}
+</style>
diff --git a/src/fabric-editor/viewcomponents/right/index.vue b/src/fabric-editor/viewcomponents/right/index.vue
new file mode 100644
index 0000000..7ac5ec8
--- /dev/null
+++ b/src/fabric-editor/viewcomponents/right/index.vue
@@ -0,0 +1,194 @@
+<script lang="ts" setup>
+// import align from '@/fabric-editor/components/align.vue';
+// import centerAlign from '@/fabric-editor/components/centerAlign.vue';
+// import flip from '@/components/flip.vue';
+
+// import clone from '@/components/clone.vue';
+// import hide from '@/components/hide.vue';
+import group from '@/fabric-editor/components/group.vue';
+import lock from '@/fabric-editor/components/lock.vue';
+import dele from '@/fabric-editor/components/del.vue';
+import previewImageList from '@/fabric-editor/components/previewImageList.vue';
+
+// import bgBar from '@/components/bgBar.vue';
+// import setSize from '@/fabric-editor/components/setSize.vue';
+// import replaceImg from '@/components/replaceImg.vue';
+// import filters from '@/components/filters.vue';
+// import imgStroke from '@/components/imgStroke.vue';
+// import elementData from '@/components/elementData.vue';
+// 鍙充晶缁勪欢
+// import attribute from '@/components/attribute.vue';
+import attributePostion from '@/fabric-editor/components/attributePostion.vue';
+import attributeTemplateParam from '@/fabric-editor/components/attributeTemplateParam.vue';
+// import attributeId from '@/components/attributeId.vue';
+// import attributeShadow from '@/components/attributeShadow.vue';
+// import attributeBorder from '@/components/attributeBorder.vue';
+// import attributeRounded from '@/components/attributeRounded.vue';
+import attributeFont from '@/fabric-editor/components/attributeFont.vue';
+// import attributeTextFloat from '@/components/attributeTextFloat.vue';
+// import attributeColor from '@/components/attributeColor.vue';
+// import attributeBarcode from '@/components/attributeBarcode.vue';
+// import attributeQrCode from '@/components/attributeQrCode.vue';
+// import cropperImg from '@/components/cropperImg.vue';
+// hooks
+import useSelectListen from '@/fabric-editor/hooks/useSelectListen';
+
+const canvasEditor: any = inject('canvasEditor');
+
+const { mixinState } = useSelectListen(canvasEditor);
+
+const attrBarShow = ref(true);
+
+// 灞炴�ч潰鏉垮紑鍏�
+const switchAttrBar = () => {
+  attrBarShow.value = !attrBarShow.value;
+};
+</script>
+
+<template>
+  <!-- 灞炴�у尯鍩� 380-->
+  <div class="right-bar" v-show="attrBarShow">
+    <div style="padding-top: 10px">
+      <!-- 鏈�夋嫨鍏冪礌鏃� 灞曠ず鑳屾櫙璁剧疆 -->
+      <div v-show="!mixinState.mSelectMode">
+        <!-- <set-size></set-size>
+        <bg-bar></bg-bar> -->
+        <preview-image-list />
+      </div>
+
+      <!-- 澶氶�夋椂灞曠ず -->
+      <div v-show="mixinState.mSelectMode === 'multiple'">
+        <!-- 鍒嗙粍 -->
+        <group></group>
+        <!-- <Divider plain></Divider> -->
+        <!-- 缁勫榻愭柟寮� -->
+        <!-- <align></align> -->
+        <!-- 灞呬腑瀵归綈 -->
+        <!-- <center-align></center-align> -->
+      </div>
+
+      <div v-show="mixinState.mSelectMode === 'one'" class="attr-item-box">
+        <!-- <h3>蹇嵎鎿嶄綔</h3> -->
+        <!-- 鍒嗙粍 -->
+        <group></group>
+        <!-- <Divider plain></Divider> -->
+        <el-divider content-position="left">
+          <h4>蹇嵎鎿嶄綔</h4>
+        </el-divider>
+        <div class="bg-item" v-show="mixinState.mSelectMode">
+          <lock></lock>
+          <dele></dele>
+          <!-- <clone></clone> -->
+          <!-- <hide></hide>
+          <edit></edit> -->
+        </div>
+        <!-- <Divider plain></Divider> -->
+        <!-- 灞呬腑瀵归綈 -->
+        <!-- <center-align></center-align> -->
+        <!-- 鏇挎崲鍥剧墖 -->
+        <!-- <replaceImg></replaceImg> -->
+        <!-- 瑁佸壀 -->
+        <!-- <cropperImg></cropperImg> -->
+        <!-- 鍥剧墖瑁佸垏 -->
+        <!-- <clip-image></clip-image> -->
+        <!-- 缈昏浆 -->
+        <!-- <flip></flip> -->
+        <!-- 鏉″舰鐮佸睘鎬� -->
+        <!-- <attributeBarcode></attributeBarcode> -->
+        <!-- 浜岀淮鐮� -->
+        <!-- <attributeQrCode></attributeQrCode> -->
+        <!-- 鍥剧墖婊ら暅 -->
+        <!-- <filters></filters> -->
+        <!-- 鍥剧墖鎻忚竟 -->
+        <!-- <imgStroke /> -->
+        <!-- 棰滆壊 -->
+        <!-- <attributeColor></attributeColor> -->
+        <attributeTemplateParam></attributeTemplateParam>
+        <!-- 瀛椾綋灞炴�� -->
+        <attributeFont></attributeFont>
+        <!-- 瀛椾綋灏忔暟鐐� -->
+        <!-- <attributeTextFloat></attributeTextFloat> -->
+        <!-- 鏂囧瓧鍐呭  -->
+        <!-- <attribute-text-content></attribute-text-content> -->
+        <!-- 浣嶇疆淇℃伅 -->
+        <attributePostion></attributePostion>
+        <!-- 闃村奖 -->
+        <!-- <attributeShadow></attributeShadow> -->
+        <!-- 杈规 -->
+        <!-- <attributeBorder></attributeBorder> -->
+        <!-- 鍦嗚 -->
+        <!-- <attributeRounded></attributeRounded> -->
+        <!-- 鍏宠仈鏁版嵁 -->
+        <!-- <attributeId></attributeId> -->
+
+        <!-- 鏂板瀛椾綋鏍峰紡浣跨敤 -->
+        <!-- <el-button @click="canvasEditor.getFontJson()" size="small">鑾峰彇鍏冪礌鏁版嵁</el-button> -->
+      </div>
+    </div>
+    <!-- <attribute v-if="state.show"></attribute> -->
+  </div>
+  <!-- 鍙充晶鍏抽棴鎸夐挳 -->
+  <div
+    :class="`close-btn right-btn ${attrBarShow && 'right-btn-open'}`"
+    @click="switchAttrBar"
+  ></div>
+</template>
+
+<style lang="scss" scoped>
+// 鍙充晶瀹瑰櫒
+
+.right-bar {
+  overflow-y: auto;
+  padding: 10px;
+  width: 330px;
+  height: 100%;
+  background: #ffffff;
+}
+
+// 灞炴�ч潰鏉挎牱寮�
+
+:deep(.attr-item) {
+  position: relative;
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  padding: 0 10px;
+  height: 40px;
+  border: none;
+  border-radius: 4px;
+  background: #f6f7f9;
+
+  .ivu-tooltip {
+    text-align: center;
+    flex: 1;
+  }
+}
+
+// 鍏抽棴鎸夐挳
+
+.close-btn {
+  position: absolute;
+  top: 50%;
+  right: -20px;
+  z-index: 1;
+  margin-top: -10px;
+  width: 20px;
+  height: 64px;
+  background-position: 50%;
+  background-repeat: no-repeat;
+  background-size: cover;
+  cursor: pointer;
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAMAAABOb9vcAAAAhFBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AAADHx8cODg50dHTx8fF2dnZ1dXWWlpZHR0c4ODhQpkZ5AAAAIXRSTlMA9t+/upkRAnPq5NXDfDEsKQjMeGlRThkMsquljTwzIWhBHpjgAAABJElEQVRYw+3YyW7CQBCEYbxig8ELGJyQkJRJyPb+75dj3zy/lD7kMH3+ZEuzSFO1mlZwhjOE2uwhVHJYMygNVwilhz2EUvNaMigledUFoE1anKYAtA9nVRuANpviOQBt0t2ZQSnZ9QxK6Qih9LSGUHkJobYlhGp6CPW4hlAVhckLhMop1InCjEK1FBYU1hSqo/BI4YXCjMIthTWFijDCCB3g7fuO4O1t/rkvQXPz/LUIzX0oAM0tQHOfCkBzC9DcuwLQXACao9Dv1yb9lsek2xaaxMcMH1x6Ff79dY0wwgj/DGv3p2tG4cX9wd55h4rCO/hk3uEs9w6QlXPIbXrfIJ6XrmVBOtJCA1YkXqVLkh1aUgyNk1fV1BxLxzpsuNLKzrME/AWr0ywwvyj83AAAAABJRU5ErkJggg==');
+
+  &.right-btn {
+    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAYAAAB5sSvuAAAAAXNSR0IArs4c6QAAAFBlWElmTU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAKKADAAQAAAABAAAAgAAAAAAobJzlAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAADf0lEQVR4Ae2cvYsTQRjGE7FQkICFB1pZRyzEJkUKmzOpBEHwX9DCQkmChf4JahewsLpWFOQUzwMRPEgEy0PLpPADvEISDrVyfZ6cK0tIZrI7u7MPMi+8mb35uPnlmXczyeXmrURRdKyibAB8Dz8pywg42if4OUnIGd7Bww8Ut+GHpEATgPEll/y8DGRMtaB8hrryl30B2HzVW1Rcgx8vQ9UqaVac+Cf67cC34C+q1erHFcc5dUsDOD/RGBWv4M/hrwG8jzJ3cwFMwlDdd/BN+BZgd5ONLtd5Ac4zfEYFld0ALMMisxUFmAQa44dHdMB+TTasdM2bxJNxI7gDP7ISWNzJE1xymhF+uBzPbyvL2NZOA+oJIO/BrfP7iEGTSNtovIrY/L6sU9mA5PoAby6DtEq87JnlWF/H7+K+v/DmUQDkc23CNxbFpAogIa/Ab/IiaQoxmOThlnkG8TiKK5UUJNNR+MMYjqUaIJnWEYuXeEFTBCTXv1hUi0HCxXYWsbirqiAhb/BBWcE9KLimDEgB68pLTMAL6oBNdcBT6oBr6oAn1O9i2a2Od/DM1Jc4KBivVOYyLHFm6f4ODAoGBV0VcB0fYjAo6KqA6/gQg0FBVwVcx4cYDAq6KuA6/v+Mwel0Wmm325XhcOgqkH08/h6cyiaTSdRoNPhvBFGtVosGg0Gq8Wk7V9IO6Pf7MzgC+oBMDcgn1Ov1vEFmAvQJmRmQkN1ut3AlnQB9QDoDErLT6RSmZC6ARULmBlgUpPxWl5uCRcVhLoBFwTFsnAGLfi10AiwazklBX/txJgV9wWVSUP7tlvwbVspOyFarVfi7ac4Vvquzfyoy95DfiwOgeQHtrUFBu0bmHkFBsz721qCgXSNzj6CgWR97a1DQrpG5R1DQrI+9NSho18jcIyho1sfauqeuoDzgN3UFv6gD7qh/cK8rA84OGygv8VO+CCkrKH3g5Q1P41BB1SV+QDia4hJvQ72LB3h6gPIH/+5CvVGsntoSPwYQzxr/VgRkJoF1wP1KwvFa4SaRPgDNI+RLT2dTwTJfB+9j/jaWden5dgIe5oNnG2O+WwCb7bXWuflliSfLlAjCh4JULHMqjaIAc0tGkhdgnM6FyXI2EV+5pXNxAeTSMSHOSzg3+H2UuVsaQKq0A/eaUmiVb9yZlOk6vJSkTCZA2bRWsonBpFOrySan+wNoJmOM0LyBGwAAAABJRU5ErkJggg==');
+    transform: rotateY(180deg);
+    right: 0px;
+  }
+
+  &.right-btn-open {
+    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAACACAMAAABOb9vcAAAAhFBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AAADHx8cODg50dHTx8fF2dnZ1dXWWlpZHR0c4ODhQpkZ5AAAAIXRSTlMA9t+/upkRAnPq5NXDfDEsKQjMeGlRThkMsquljTwzIWhBHpjgAAABJElEQVRYw+3YyW7CQBCEYbxig8ELGJyQkJRJyPb+75dj3zy/lD7kMH3+ZEuzSFO1mlZwhjOE2uwhVHJYMygNVwilhz2EUvNaMigledUFoE1anKYAtA9nVRuANpviOQBt0t2ZQSnZ9QxK6Qih9LSGUHkJobYlhGp6CPW4hlAVhckLhMop1InCjEK1FBYU1hSqo/BI4YXCjMIthTWFijDCCB3g7fuO4O1t/rkvQXPz/LUIzX0oAM0tQHOfCkBzC9DcuwLQXACao9Dv1yb9lsek2xaaxMcMH1x6Ff79dY0wwgj/DGv3p2tG4cX9wd55h4rCO/hk3uEs9w6QlXPIbXrfIJ6XrmVBOtJCA1YkXqVLkh1aUgyNk1fV1BxLxzpsuNLKzrME/AWr0ywwvyj83AAAAABJRU5ErkJggg==');
+    right: 330px;
+  }
+}
+</style>
diff --git a/src/fabric-editor/viewcomponents/top/index.vue b/src/fabric-editor/viewcomponents/top/index.vue
new file mode 100644
index 0000000..4f340ec
--- /dev/null
+++ b/src/fabric-editor/viewcomponents/top/index.vue
@@ -0,0 +1,89 @@
+<template>
+  <el-header>
+    <div class="left">
+      <el-divider direction="vertical" />
+
+      <!-- 瀵煎叆 -->
+      <!-- <import-Json></import-Json> -->
+      <!-- <Divider type="vertical" /> -->
+      <!-- <import-file></import-file> -->
+      <!-- <Divider type="vertical" /> -->
+      <!-- <Button type="text" to="/template" target="_blank">鍏ㄩ儴妯℃澘</Button>
+      <Divider type="vertical" /> -->
+
+      <!-- <myTemplName></myTemplName> -->
+      <!-- 鏍囧昂寮�鍏� -->
+      <el-tooltip :content="'缃戞牸'">
+        <el-switch v-model="toggleModel" size="small" class="switch"></el-switch>
+      </el-tooltip>
+      <el-divider direction="vertical" />
+      <!-- <history></history> -->
+    </div>
+
+    <div class="center" v-if="templateImageList.length > 0">
+      <el-pagination
+        layout="prev, pager, next"
+        :page-count="templateImageList.length"
+        :current-page="templateEditState.currentImageIndex + 1"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <div class="right">
+      <!-- <a href="https://pro.kuaitu.cc/" target="_blank" alt="鍟嗕笟鐗�">
+        <img width="15" :src="proIcon" alt="vue-fbric-editor" />
+      </a> -->
+      <!-- 绠$悊鍛樻ā寮� -->
+      <!-- <admin /> -->
+      <!-- 棰勮 -->
+      <!-- <previewCurrent /> -->
+      <!-- <waterMark /> -->
+      <saveV2></saveV2>
+      <!-- <login></login> -->
+      <!-- <lang></lang> -->
+    </div>
+  </el-header>
+</template>
+
+<script name="Top" setup lang="ts">
+import history from '@/fabric-editor/components/history.vue';
+import saveV2 from '@/fabric-editor/components/saveV2.vue';
+import { useTemplateDetailContext, useCanvasActions } from '../../hooks/context';
+
+const props = defineProps(['ruler']);
+const emit = defineEmits(['update:ruler']);
+
+const toggleModel = computed({
+  get() {
+    return props.ruler;
+  },
+  set(value) {
+    emit('update:ruler', value);
+  },
+});
+
+const { templateImageList, templateEditState } = useTemplateDetailContext();
+const { toggleCanvas } = useCanvasActions();
+
+function handleCurrentChange(val: number) {
+  toggleCanvas(val - 1);
+}
+</script>
+
+<style lang="scss" scoped>
+.left,
+.right {
+  display: flex;
+  align-items: center;
+
+  img {
+    display: block;
+    margin-right: 10px;
+  }
+}
+
+.center {
+  display: flex;
+  align-items: center;
+}
+</style>
diff --git a/src/router/index.ts b/src/router/index.ts
index e4bb571..8971257 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -105,6 +105,16 @@
     },
   },
   {
+    path: '/ProtocolManage/TemplateKeyEdit/:id',
+    name: 'TemplateKeyEdit',
+    hidden: true,
+    component: () => import('@/views/ProtocolManage/TemplateKeyEdit.vue'),
+    meta: {
+      title: '鐢靛瓙绛炬ā鏉垮埗鐗�',
+      rank: 102,
+    },
+  },
+  {
     path: '/Error',
     component: ErrorLayout,
     redirect: '/Error/401',
diff --git a/src/services/api/electronSign.ts b/src/services/api/electronSign.ts
index b649bcb..7e0325b 100644
--- a/src/services/api/electronSign.ts
+++ b/src/services/api/electronSign.ts
@@ -86,6 +86,26 @@
   );
 }
 
+/** 鏌ヨ涓汉瀹炲悕缁撴灉 GET /api/user/electronSign/getPersonalUserRealResult */
+export async function getPersonalUserRealResult(
+  // 鍙犲姞鐢熸垚鐨凱aram绫诲瀷 (闈瀊ody鍙傛暟swagger榛樿娌℃湁鐢熸垚瀵硅薄)
+  params: API.APIgetPersonalUserRealResultParams,
+  options?: API.RequestConfig
+) {
+  return request<API.GetPersonalUserRealResultQueryResult>(
+    '/api/user/electronSign/getPersonalUserRealResult',
+    {
+      method: 'GET',
+      params: {
+        ...params,
+        request: undefined,
+        ...params['request'],
+      },
+      ...(options || {}),
+    }
+  );
+}
+
 /** 涓汉浜鸿劯瀹炲悕璁よ瘉 POST /api/user/electronSign/personalUserFaceReal */
 export async function personalUserFaceReal(
   body: API.PersonalUserFaceRealCommand,
@@ -131,6 +151,21 @@
   });
 }
 
+/** 鍚堝悓鍒剁増 POST /api/user/electronSign/saveContractTemplateValues */
+export async function saveContractTemplateValues(
+  body: API.SaveContractTemplateValuesCommand,
+  options?: API.RequestConfig
+) {
+  return request<string>('/api/user/electronSign/saveContractTemplateValues', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json-patch+json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
 /** 鍙戦�佷釜浜轰笁瑕佺礌瀹炲悕鐭俊 POST /api/user/electronSign/sendPersonalUserIdentity3RealSms */
 export async function sendPersonalUserIdentity3RealSms(
   body: API.SendPersonalUserIdentity3RealSmsCommand,
diff --git a/src/services/api/index.ts b/src/services/api/index.ts
index 39390a8..2973753 100644
--- a/src/services/api/index.ts
+++ b/src/services/api/index.ts
@@ -6,15 +6,15 @@
 import * as user from './user';
 import * as role from './role';
 import * as enterprise from './enterprise';
+import * as electronSign from './electronSign';
 import * as resource from './resource';
 import * as task from './task';
-import * as ocrUtils from './ocrUtils';
 import * as dictionary from './dictionary';
 import * as userResume from './userResume';
 import * as auth from './auth';
 import * as taskCheckReceive from './taskCheckReceive';
-import * as electronSign from './electronSign';
 import * as taskUser from './taskUser';
+import * as ocrUtils from './ocrUtils';
 import * as menu from './menu';
 import * as logRecords from './logRecords';
 import * as fileUtils from './fileUtils';
@@ -23,15 +23,15 @@
   user,
   role,
   enterprise,
+  electronSign,
   resource,
   task,
-  ocrUtils,
   dictionary,
   userResume,
   auth,
   taskCheckReceive,
-  electronSign,
   taskUser,
+  ocrUtils,
   menu,
   logRecords,
   fileUtils,
diff --git a/src/services/api/ocrUtils.ts b/src/services/api/ocrUtils.ts
index 4601bae..75d1669 100644
--- a/src/services/api/ocrUtils.ts
+++ b/src/services/api/ocrUtils.ts
@@ -2,17 +2,44 @@
 // @ts-ignore
 import { request } from '@/utils/request';
 
-/** 鏂囧瓧璇嗗埆钀ヤ笟鎵х収 GET /api/common/ocrUtils/getLicenseOcr */
-export async function getLicenseOcr(
-  // 鍙犲姞鐢熸垚鐨凱aram绫诲瀷 (闈瀊ody鍙傛暟swagger榛樿娌℃湁鐢熸垚瀵硅薄)
-  params: API.APIgetLicenseOcrParams,
+/** 鏂囧瓧璇嗗埆韬唤璇佽儗闈� POST /api/common/ocrUtils/getIdentityBackOcr */
+export async function getIdentityBackOcr(
+  body: API.GetIdentityBackOcrCommand,
   options?: API.RequestConfig
 ) {
-  return request<API.GetLicenseOcrCommandResult>('/api/common/ocrUtils/getLicenseOcr', {
-    method: 'GET',
-    params: {
-      ...params,
+  return request<API.GetIdentityBackOcrCommandResult>('/api/common/ocrUtils/getIdentityBackOcr', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json-patch+json',
     },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 鏂囧瓧璇嗗埆韬唤璇佹闈� POST /api/common/ocrUtils/getIdentityFrontOcr */
+export async function getIdentityFrontOcr(
+  body: API.GetIdentityFrontOcrCommand,
+  options?: API.RequestConfig
+) {
+  return request<API.GetIdentityFrontOcrCommandResult>('/api/common/ocrUtils/getIdentityFrontOcr', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json-patch+json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 鏂囧瓧璇嗗埆钀ヤ笟鎵х収 POST /api/common/ocrUtils/getLicenseOcr */
+export async function getLicenseOcr(body: API.GetLicenseOcrCommand, options?: API.RequestConfig) {
+  return request<API.GetLicenseOcrCommandResult>('/api/common/ocrUtils/getLicenseOcr', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json-patch+json',
+    },
+    data: body,
     ...(options || {}),
   });
 }
diff --git a/src/services/api/typings.d.ts b/src/services/api/typings.d.ts
index 04356b2..9b8595f 100644
--- a/src/services/api/typings.d.ts
+++ b/src/services/api/typings.d.ts
@@ -98,17 +98,6 @@
     url?: string;
   }
 
-  interface APIgetLicenseOcrParams {
-    /** 閫氶亾 */
-    access?: EnumOcrAccess;
-    /** 鍦烘櫙 */
-    scene?: string;
-    /** 閾炬帴鍦板潃 */
-    url?: string;
-    /** 鏄惁涓轰簯瀛樺偍鐩稿鍦板潃 */
-    isOssUrl?: boolean;
-  }
-
   interface APIgetMenuParams {
     /** Id */
     id?: string;
@@ -128,6 +117,11 @@
   interface APIgetPersonalLoginInfoParams {
     /** 鏌ヨ涓汉鐢ㄦ埛鐧诲綍淇℃伅 */
     request?: GetPersonalLoginInfoQuery;
+  }
+
+  interface APIgetPersonalUserRealResultParams {
+    /** 鏌ヨ涓汉瀹炲悕缁撴灉 */
+    request?: GetPersonalUserRealResultQuery;
   }
 
   interface APIgetResourceFieldsParams {
@@ -242,6 +236,28 @@
     address?: string;
     /** 缁忚惀鑼冨洿 */
     mainBusiness?: string;
+  }
+
+  interface BaiduOcrIdentityBackResultModel {
+    /** 澶辨晥鏃ユ湡 */
+    expiryDate?: string;
+    /** 绛惧彂鏈哄叧 */
+    issueAuthority?: string;
+    /** 绛惧彂鏃ユ湡 */
+    issueDate?: string;
+  }
+
+  interface BaiduOcrIdentityFrontResultModel {
+    name?: string;
+    /** 韬唤璇佸彿 */
+    identity?: string;
+    gender?: EnumUserGender;
+    /** 鐢熸棩 */
+    birthday?: string;
+    /** 姘戞棌 */
+    nation?: string;
+    /** 浣忓潃 */
+    address?: string;
   }
 
   interface BindWxmpUserInfoCommand {
@@ -485,6 +501,17 @@
     Identity4 = 20,
     /**鍒疯劯璁よ瘉 */
     Face = 30,
+  }
+
+  enum EnumPersonalUserRealStatus {
+    /**鏈疄鍚� */
+    UnReal = 0,
+    /**鏍¢獙涓� */
+    Checking = 10,
+    /**瀹炲悕澶辫触 */
+    Fail = 99,
+    /**宸插疄鍚� */
+    Real = 100,
   }
 
   enum EnumRealAccess {
@@ -1026,6 +1053,42 @@
     timestamp?: number;
   }
 
+  interface FriendlyResultGetIdentityBackOcrCommandResult {
+    /** 璺熻釜Id */
+    traceId?: string;
+    /** 鐘舵�佺爜 */
+    code?: number;
+    /** 閿欒鐮� */
+    errorCode?: string;
+    data?: GetIdentityBackOcrCommandResult;
+    /** 鎵ц鎴愬姛 */
+    success?: boolean;
+    /** 閿欒淇℃伅 */
+    msg?: any;
+    /** 闄勫姞鏁版嵁 */
+    extras?: any;
+    /** 鏃堕棿鎴� */
+    timestamp?: number;
+  }
+
+  interface FriendlyResultGetIdentityFrontOcrCommandResult {
+    /** 璺熻釜Id */
+    traceId?: string;
+    /** 鐘舵�佺爜 */
+    code?: number;
+    /** 閿欒鐮� */
+    errorCode?: string;
+    data?: GetIdentityFrontOcrCommandResult;
+    /** 鎵ц鎴愬姛 */
+    success?: boolean;
+    /** 閿欒淇℃伅 */
+    msg?: any;
+    /** 闄勫姞鏁版嵁 */
+    extras?: any;
+    /** 鏃堕棿鎴� */
+    timestamp?: number;
+  }
+
   interface FriendlyResultGetLicenseOcrCommandResult {
     /** 璺熻釜Id */
     traceId?: string;
@@ -1160,6 +1223,24 @@
     /** 閿欒鐮� */
     errorCode?: string;
     data?: GetPersonalUserInfosQueryResult;
+    /** 鎵ц鎴愬姛 */
+    success?: boolean;
+    /** 閿欒淇℃伅 */
+    msg?: any;
+    /** 闄勫姞鏁版嵁 */
+    extras?: any;
+    /** 鏃堕棿鎴� */
+    timestamp?: number;
+  }
+
+  interface FriendlyResultGetPersonalUserRealResultQueryResult {
+    /** 璺熻釜Id */
+    traceId?: string;
+    /** 鐘舵�佺爜 */
+    code?: number;
+    /** 閿欒鐮� */
+    errorCode?: string;
+    data?: GetPersonalUserRealResultQueryResult;
     /** 鎵ц鎴愬姛 */
     success?: boolean;
     /** 閿欒淇℃伅 */
@@ -2103,6 +2184,10 @@
     name?: string;
     /** 妯℃澘 */
     file?: string;
+    /** 鍒剁増妯℃澘鐓х墖 */
+    templateEditData?: string;
+    /** 鍒剁増鍙橀噺JSON */
+    templateJsonData?: string;
     /** 涓氬姟缂栫爜 */
     code?: string;
     access?: EnumElectronSignAccess;
@@ -2582,6 +2667,48 @@
     createdTime?: string;
   }
 
+  interface GetIdentityBackOcrCommand {
+    access?: EnumOcrAccess;
+    /** 鍦烘櫙 */
+    scene?: string;
+    /** 閾炬帴鍦板潃 */
+    url?: string;
+    /** 鏄惁涓轰簯瀛樺偍鐩稿鍦板潃 */
+    isOssUrl?: boolean;
+  }
+
+  interface GetIdentityBackOcrCommandResult {
+    /** 鍦板潃 */
+    url?: string;
+    model?: BaiduOcrIdentityBackResultModel;
+  }
+
+  interface GetIdentityFrontOcrCommand {
+    access?: EnumOcrAccess;
+    /** 鍦烘櫙 */
+    scene?: string;
+    /** 閾炬帴鍦板潃 */
+    url?: string;
+    /** 鏄惁涓轰簯瀛樺偍鐩稿鍦板潃 */
+    isOssUrl?: boolean;
+  }
+
+  interface GetIdentityFrontOcrCommandResult {
+    /** 鍦板潃 */
+    url?: string;
+    model?: BaiduOcrIdentityFrontResultModel;
+  }
+
+  interface GetLicenseOcrCommand {
+    access?: EnumOcrAccess;
+    /** 鍦烘櫙 */
+    scene?: string;
+    /** 閾炬帴鍦板潃 */
+    url?: string;
+    /** 鏄惁涓轰簯瀛樺偍鐩稿鍦板潃 */
+    isOssUrl?: boolean;
+  }
+
   interface GetLicenseOcrCommandResult {
     /** 鍦板潃 */
     url?: string;
@@ -2989,6 +3116,41 @@
     hireTime?: string;
     /** 鏈�杩戠绾︽椂闂� */
     signContractTime?: string;
+  }
+
+  type GetPersonalUserRealResultQuery = Record<string, any>;
+
+  interface GetPersonalUserRealResultQueryResult {
+    /** 鏄惁瀹炲悕 */
+    isReal?: boolean;
+    /** 瀹炲悕鏃堕棿 */
+    realTime?: string;
+    realMethod?: EnumUserRealMethod;
+    /** 濮撳悕 */
+    name?: string;
+    /** 鎵嬫満鍙� */
+    phoneNumber?: string;
+    /** 韬唤璇佸彿 */
+    identity?: string;
+    /** 韬唤璇佷汉鍍忛潰 */
+    identityImg?: string;
+    /** 韬唤璇佸浗寰介潰 */
+    identityBackImg?: string;
+    gender?: EnumUserGender;
+    /** 鐢熸棩 */
+    birthday?: string;
+    /** 骞撮緞 */
+    age?: number;
+    /** 閾惰鍗″彿 */
+    bankCard?: string;
+    /** 閾惰鍗$収鐗� */
+    bankCardImg?: string;
+    realAccess?: EnumRealAccess;
+    realStatus?: EnumPersonalUserRealStatus;
+    /** 瀹炲悕澶辫触娑堟伅 */
+    realFailMessage?: string;
+    /** 浜鸿劯瀹炲悕璁よ瘉鍦板潃 */
+    faceRealUrl?: string;
   }
 
   interface GetResourceFieldsQueryResultItem {
@@ -3850,18 +4012,27 @@
     name: string;
     /** 妯℃澘 */
     file: string;
+    /** 鍒剁増妯℃澘鐓х墖 */
+    templateEditData?: string;
     /** 涓氬姟缂栫爜 */
     code?: string;
     access?: EnumElectronSignAccess;
     /** 妯℃澘Id */
     templateId?: string;
-    /** 鍙橀噺 */
-    values?: SaveContractTemplateCommandValue[];
     /** Id */
     id?: string;
   }
 
-  interface SaveContractTemplateCommandValue {
+  interface SaveContractTemplateValuesCommand {
+    /** 妯℃澘Id */
+    id: string;
+    /** 鍒剁増鍙橀噺JSON */
+    templateJsonData?: string;
+    /** 鍙橀噺 */
+    values: SaveContractTemplateValuesCommandItem[];
+  }
+
+  interface SaveContractTemplateValuesCommandItem {
     /** Id */
     id?: string;
     type?: EnumContractTemplateValueType;
diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts
index b7ac8c7..e0e7104 100644
--- a/src/utils/common/index.ts
+++ b/src/utils/common/index.ts
@@ -13,3 +13,4 @@
 export * from './file';
 export * from './vueHelper';
 export * from './encrypt';
+export * from './pdf';
diff --git a/src/utils/common/pdf.ts b/src/utils/common/pdf.ts
new file mode 100644
index 0000000..671cfc3
--- /dev/null
+++ b/src/utils/common/pdf.ts
@@ -0,0 +1,77 @@
+import { BoleOss, Message } from '@bole-core/core';
+import { RichEditorUtils } from '@bole-core/components';
+import { OssManager } from '../oss';
+import { ElLoading } from 'element-plus';
+
+pdfjsLib.GlobalWorkerOptions.workerSrc =
+  'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.js';
+
+export async function resolvePdfByUrl(url: string) {
+  let pdf = await pdfjsLib.getDocument(url).promise;
+
+  return pdf;
+}
+
+export type ConvertPdfToImageItem = BoleOss.PutObjectResult & {
+  width: number;
+  height: number;
+};
+
+export async function convertPdfToImage(pdfUrl: string) {
+  let loadingInstance: ReturnType<typeof ElLoading.service>;
+  try {
+    loadingInstance = ElLoading.service({
+      fullscreen: true,
+      lock: false,
+      background: 'transparent',
+    });
+    const pdf = await resolvePdfByUrl(pdfUrl);
+    let tasks: Promise<ConvertPdfToImageItem>[] = [];
+    for (let i = 0; i < pdf.numPages; i++) {
+      const page = await pdf.getPage(i + 1);
+      const viewport = page.getViewport({ scale: 1 });
+      tasks.push(
+        new Promise(async (resolve, reject) => {
+          try {
+            let base64 = await generateBase64ByPage(page);
+            let file = RichEditorUtils.base64ToFile(base64);
+            let res = await OssManager.asyncUpload({
+              file: file,
+            });
+            resolve({
+              ...res,
+              width: viewport.width,
+              height: viewport.height,
+            });
+          } catch (error) {
+            reject(error);
+          }
+        })
+      );
+    }
+    return Promise.all(tasks);
+  } catch (error) {
+    Message.errorMessage(error);
+    throw new Error(error);
+  } finally {
+    if (loadingInstance) {
+      loadingInstance.close();
+    }
+  }
+}
+
+async function generateBase64ByPage(page) {
+  const viewport = page.getViewport({ scale: 1 });
+  let canvas = document.createElement('canvas');
+  const context = canvas.getContext('2d');
+  canvas.height = viewport.height;
+  canvas.width = viewport.width;
+  const renderContext = {
+    canvasContext: context,
+    viewport: viewport,
+  };
+  await page.render(renderContext).promise;
+  let base64 = canvas.toDataURL('image/png');
+  canvas = null;
+  return base64;
+}
diff --git a/src/views/DictionaryManage/DataDictionary.vue b/src/views/DictionaryManage/DataDictionary.vue
index 86acd05..69e84cf 100644
--- a/src/views/DictionaryManage/DataDictionary.vue
+++ b/src/views/DictionaryManage/DataDictionary.vue
@@ -158,6 +158,7 @@
       sort: row.sort,
       isDisabled: row.isDisabled,
       field1: row.field1,
+      field3: row.field3,
       field2: convertApi2FormUrlOnlyOne(row.field2),
     });
   } else {
@@ -178,6 +179,7 @@
     isDisabled: false,
     field1: '',
     field2: [] as UploadUserFile[],
+    field3: '',
     title: '鏂板瀛楀吀',
   },
   editTitle: '缂栬緫瀛楀吀',
@@ -193,6 +195,7 @@
       isDisabled: editForm.isDisabled,
       field1: editForm.field1,
       field2: editForm.field2?.[0]?.path ?? '',
+      field3: editForm.field3,
     };
     if (editForm.id) {
       params.id = editForm.id;
diff --git a/src/views/DictionaryManage/components/AddOrEditDictionaryDialog.vue b/src/views/DictionaryManage/components/AddOrEditDictionaryDialog.vue
index e4f873e..52032b2 100644
--- a/src/views/DictionaryManage/components/AddOrEditDictionaryDialog.vue
+++ b/src/views/DictionaryManage/components/AddOrEditDictionaryDialog.vue
@@ -6,7 +6,7 @@
     destroy-on-close
     draggable
   >
-    <ProForm :model="form" ref="dialogForm" label-width="90px">
+    <ProForm :model="form" ref="dialogForm" label-width="120px">
       <ProFormItemV2
         label="琛屼笟绫诲瀷:"
         prop="field1"
@@ -38,6 +38,14 @@
       </ProFormItemV2>
       <ProFormItemV2 label="缂栧彿:" prop="code" :check-rules="[{ message: '璇疯緭鍏ョ紪鍙�' }]">
         <ProFormText v-model.trim="form.code" :disabled="!!form.id"></ProFormText>
+      </ProFormItemV2>
+      <ProFormItemV2
+        label="鍙傛暟瀛楁鍚�:"
+        prop="field3"
+        :check-rules="[{ message: '璇疯緭鍏ュ弬鏁板瓧娈靛悕' }]"
+        v-if="category?.data?.code === CategoryCode.ElectronSignParam"
+      >
+        <ProFormText v-model.trim="form.field3"></ProFormText>
       </ProFormItemV2>
       <ProFormItemV2
         label="鍥剧墖:"
@@ -96,6 +104,7 @@
   isDisabled: boolean;
   field1?: string;
   field2?: UploadUserFile[];
+  field3?: string;
 };
 
 const form = defineModel<Form>('form');
diff --git a/src/views/ProtocolManage/EditTemplate.vue b/src/views/ProtocolManage/EditTemplate.vue
index 257d4a3..dad58d8 100644
--- a/src/views/ProtocolManage/EditTemplate.vue
+++ b/src/views/ProtocolManage/EditTemplate.vue
@@ -76,7 +76,13 @@
 import * as electronSignServices from '@/services/api/electronSign';
 import AddOrEditTemplateDialog from './components/AddOrEditTemplateDialog.vue';
 import { downloadFileByUrl, Message } from '@bole-core/core';
-import { convertApi2FormUrlOnlyOne, format, setOSSLink } from '@/utils';
+import {
+  convertApi2FormUrlOnlyOne,
+  convertPdfToImage,
+  format,
+  openLink,
+  setOSSLink,
+} from '@/utils';
 import { EnumContractTemplateStatus, EnumContractTemplateStatusText } from '@/constants';
 
 defineOptions({
@@ -84,11 +90,18 @@
 });
 
 const operationBtnMap: Record<string, OperationBtnType> = {
-  editBtn: { emits: { onClick: (role) => openDialog(role) } },
-  editTemplateBtn: {
+  editBtn: {
     emits: { onClick: (role) => openDialog(role) },
     extraProps: {
-      hide: (row) => false,
+      hide: (row: API.GetEnterpriseContractTemplatesQueryResultItem) =>
+        row.status !== EnumContractTemplateStatus.Completed,
+    },
+  },
+  editTemplateBtn: {
+    emits: { onClick: (role) => goEditTemplate(role) },
+    extraProps: {
+      hide: (row: API.GetEnterpriseContractTemplatesQueryResultItem) =>
+        row.status !== EnumContractTemplateStatus.Wait,
     },
   },
   downloadBtn: {
@@ -98,7 +111,8 @@
     emits: { onClick: (role) => handleDelete(role) },
     props: { type: 'danger' },
     extraProps: {
-      hide: (row) => false,
+      hide: (row: API.GetEnterpriseContractTemplatesQueryResultItem) =>
+        row.status !== EnumContractTemplateStatus.Completed,
     },
   },
 
@@ -206,6 +220,7 @@
 async function handleAddOrEdit() {
   try {
     let isEdit = !!editForm.id;
+    let pdfToImage = await convertPdfToImage(editForm.file?.[0].url);
     let params: API.SaveContractTemplateCommand = {
       name: editForm.name,
       file: editForm.file[0]?.path ?? '',
@@ -213,17 +228,16 @@
       code: editForm.code,
       access: editForm.access,
       templateId: editForm.templateId,
-      // values: [
-      //   {
-      //     id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
-      //     type: 10,
-      //     recorder: 10,
-      //     userType: 10,
-      //     label: 'string',
-      //     name: 'string',
-      //     required: true,
-      //   },
-      // ],
+      templateEditData: JSON.stringify(
+        pdfToImage.map(
+          (x) =>
+            ({
+              path: x.path,
+              width: x.width,
+              height: x.height,
+            } as TemplateEditDataItem)
+        )
+      ),
     };
     if (isEdit) {
       params.id = editForm.id;
@@ -253,6 +267,19 @@
   } catch (error) {}
 }
 
+const router = useRouter();
+
+function goEditTemplate(row: API.GetEnterpriseContractTemplatesQueryResultItem) {
+  openLink(
+    router.resolve({
+      name: 'TemplateKeyEdit',
+      params: {
+        id: row.id ?? '',
+      },
+    }).fullPath
+  );
+}
+
 const { openLogDialog, logDialogProps } = useOpenLogDialog({
   service: async ({ pageIndex, pageSize }, extraParamState) => {
     try {
diff --git a/src/views/ProtocolManage/TemplateKeyEdit.vue b/src/views/ProtocolManage/TemplateKeyEdit.vue
new file mode 100644
index 0000000..980fae2
--- /dev/null
+++ b/src/views/ProtocolManage/TemplateKeyEdit.vue
@@ -0,0 +1,18 @@
+<template>
+  <AppContainer>
+    <FabricEditorView />
+  </AppContainer>
+</template>
+
+<script setup lang="ts">
+import FabricEditorView from '@/fabric-editor/index.vue';
+import { AppContainer } from '@bole-core/components';
+
+defineOptions({
+  name: 'TemplateKeyEdit',
+});
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+</style>
diff --git a/types/global.d.ts b/types/global.d.ts
index 4c54821..4c826eb 100644
--- a/types/global.d.ts
+++ b/types/global.d.ts
@@ -15,6 +15,9 @@
     };
     lastBuildTime: string;
   };
+
+   const pdfjsLib: any
+
   interface Window {
     webkitCancelAnimationFrame: (handle: number) => void;
     mozCancelAnimationFrame: (handle: number) => void;

--
Gitblit v1.9.1